This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit 435cc244aa093921f698f31705b59d8431aaa015
Author: Logic <[email protected]>
AuthorDate: Fri May 29 02:08:31 2026 +0800

    feat(web-next): align shared UI primitive parity
    
    Validation: vitest run app/loading.test.tsx 
packages/design-tokens/src/tokens.test.ts 
components/ui/cold-code-editor.test.tsx 
components/ui/cold-confirm-dialog.test.tsx 
components/ui/date-time-range.test.tsx 
components/ui/label-record-input.test.tsx components/ui/number-stepper.test.tsx 
components/ui/search-row.test.tsx components/ui/select.test.tsx 
components/ui/tag-input.test.tsx components/workbench/overlay-dialog.test.tsx 
components/workbench/workspace-tab-strip.test.tsx app/ui-la [...]
---
 web-next/app/globals.css                           | 39 +++++++--
 web-next/app/loading.test.tsx                      | 12 +--
 web-next/app/loading.tsx                           | 25 ++----
 web-next/components/ui/cold-code-editor.test.tsx   | 76 ++++++++++++++++-
 web-next/components/ui/cold-code-editor.tsx        | 92 +++++++++++++++++++--
 .../components/ui/cold-confirm-dialog.test.tsx     | 44 ++++++++++
 web-next/components/ui/cold-confirm-dialog.tsx     |  9 +-
 web-next/components/ui/date-time-range.test.tsx    | 49 +++++++++++
 web-next/components/ui/date-time-range.tsx         | 95 ++++++++++++++++++----
 web-next/components/ui/label-record-input.test.tsx |  5 ++
 web-next/components/ui/label-record-input.tsx      | 13 ++-
 web-next/components/ui/number-stepper.test.tsx     |  2 +
 web-next/components/ui/number-stepper.tsx          |  9 +-
 web-next/components/ui/search-row.test.tsx         | 20 +++++
 web-next/components/ui/search-row.tsx              |  9 +-
 web-next/components/ui/select.test.tsx             | 25 ++++++
 web-next/components/ui/select.tsx                  | 39 ++++++++-
 web-next/components/ui/tag-input.test.tsx          |  3 +
 web-next/components/ui/tag-input.tsx               |  9 +-
 .../components/workbench/overlay-dialog.test.tsx   | 32 ++++++++
 web-next/components/workbench/overlay-dialog.tsx   | 44 ++++++----
 .../workbench/workspace-tab-strip.test.tsx         |  3 +
 22 files changed, 570 insertions(+), 84 deletions(-)

diff --git a/web-next/app/globals.css b/web-next/app/globals.css
index 47c1c39363..9dd5cb7b76 100644
--- a/web-next/app/globals.css
+++ b/web-next/app/globals.css
@@ -1,3 +1,6 @@
+@import '../packages/design-tokens/src/tokens.css';
+@import '../packages/design-tokens/src/themes.css';
+
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
@@ -61,6 +64,30 @@
   --hb-shadow: var(--ops-panel-shadow);
   --hb-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
   --hb-mono: 'SFMono-Regular', ui-monospace, Menlo, Monaco, Consolas, 
monospace;
+
+  --hz-ui-canvas: #0b0d10;
+  --hz-ui-surface: #0d1014;
+  --hz-ui-surface-soft: #10141a;
+  --hz-ui-surface-raised: #131922;
+  --hz-ui-surface-graphite: #0c0e12;
+  --hz-ui-surface-muted: rgba(255, 255, 255, 0.018);
+  --hz-ui-control: #0e1218;
+  --hz-ui-code: #090b0e;
+  --hz-ui-line-strong: rgba(166, 178, 195, 0.135);
+  --hz-ui-line: rgba(166, 178, 195, 0.085);
+  --hz-ui-line-soft: rgba(166, 178, 195, 0.052);
+  --hz-ui-line-faint: rgba(166, 178, 195, 0.032);
+  --hz-ui-active: rgba(76, 92, 148, 0.22);
+  --hz-ui-active-soft: rgba(76, 92, 148, 0.095);
+  --hz-ui-accent: #7c93db;
+  --hz-ui-accent-muted: rgba(124, 147, 219, 0.32);
+  --hz-ui-action-primary: #4f6bdc;
+  --hz-ui-action-primary-hover: #5d78ee;
+  --hz-ui-action-danger: #7f232f;
+  --hz-ui-action-danger-hover: #9b2a39;
+  --hz-ui-scrollbar-size: 7px;
+  --hz-ui-scrollbar-thumb: rgba(166, 178, 195, 0.18);
+  --hz-ui-scrollbar-thumb-hover: rgba(166, 178, 195, 0.3);
 }
 
 html[data-theme='light-ops'],
@@ -499,15 +526,15 @@ body[data-theme='compact'] {
   body,
   .hb-scrollbar {
     scrollbar-width: thin;
-    scrollbar-color: hsl(var(--border)) transparent;
+    scrollbar-color: var(--hz-ui-scrollbar-thumb) transparent;
     scrollbar-gutter: stable;
   }
 
   html::-webkit-scrollbar,
   body::-webkit-scrollbar,
   .hb-scrollbar::-webkit-scrollbar {
-    width: 10px;
-    height: 10px;
+    width: var(--hz-ui-scrollbar-size);
+    height: var(--hz-ui-scrollbar-size);
   }
 
   html::-webkit-scrollbar-track,
@@ -519,16 +546,16 @@ body[data-theme='compact'] {
   html::-webkit-scrollbar-thumb,
   body::-webkit-scrollbar-thumb,
   .hb-scrollbar::-webkit-scrollbar-thumb {
-    border: 2px solid transparent;
+    border: 1px solid transparent;
     border-radius: 999px;
-    background: linear-gradient(180deg, hsl(var(--accent)), 
hsl(var(--secondary)));
+    background: var(--hz-ui-scrollbar-thumb);
     background-clip: padding-box;
   }
 
   html::-webkit-scrollbar-thumb:hover,
   body::-webkit-scrollbar-thumb:hover,
   .hb-scrollbar::-webkit-scrollbar-thumb:hover {
-    background: linear-gradient(180deg, hsl(var(--ring) / 0.5), 
hsl(var(--accent)));
+    background: var(--hz-ui-scrollbar-thumb-hover);
     background-clip: padding-box;
   }
 
diff --git a/web-next/app/loading.test.tsx b/web-next/app/loading.test.tsx
index a889d2339a..782034c37a 100644
--- a/web-next/app/loading.test.tsx
+++ b/web-next/app/loading.test.tsx
@@ -3,16 +3,18 @@ import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it } from 'vitest';
 
 describe('global app loading shell', () => {
-  it('renders a visible operator-facing route loading state instead of a blank 
screen', async () => {
+  it('keeps route pending feedback quiet instead of showing a full Loading 
workspace panel', async () => {
     const { default: Loading } = await import('./loading');
     const html = renderToStaticMarkup(<Loading />);
 
-    
expect(html).toContain('data-app-route-loading="global-workbench-loading"');
-    expect(html).toContain('data-app-route-loading-spinner="true"');
+    expect(html).toContain('data-app-route-loading="quiet-route-pending"');
+    expect(html).toContain('data-app-route-loading-indicator="true"');
     expect(html).toContain('role="status"');
     expect(html).toContain('aria-busy="true"');
-    expect(html).toContain('正在加载工作台');
-    expect(html).toContain('正在准备页面数据');
+    expect(html).not.toContain('Loading workspace');
+    expect(html).not.toContain('Preparing page data');
+    expect(html).not.toContain('rounded-[4px]');
+    expect(html).not.toContain('min-h-[360px]');
     expect(html).not.toContain('/api/');
     expect(html).not.toContain('http://localhost');
   });
diff --git a/web-next/app/loading.tsx b/web-next/app/loading.tsx
index d0a8f5d27a..f447413db6 100644
--- a/web-next/app/loading.tsx
+++ b/web-next/app/loading.tsx
@@ -1,28 +1,21 @@
 import React from 'react';
-import { Loader2 } from 'lucide-react';
 
 export default function Loading() {
   return (
     <main
-      data-app-route-loading="global-workbench-loading"
+      data-app-route-loading="quiet-route-pending"
       role="status"
       aria-busy="true"
       aria-live="polite"
-      className="min-h-[calc(100vh-56px)] bg-[#07090b] px-6 py-6 
text-[#e8edf5]"
+      aria-label="Route pending"
+      className="min-h-[calc(100vh-56px)] bg-[#07090b]"
     >
-      <section className="mx-auto flex min-h-[360px] w-full max-w-[1600px] 
items-center justify-center rounded-[4px] border border-[#252b35] bg-[#0d1015] 
px-6 py-12 shadow-[0_18px_60px_rgba(0,0,0,0.28)]">
-        <div className="flex max-w-[420px] flex-col items-center text-center">
-          <Loader2
-            data-app-route-loading-spinner="true"
-            className="h-8 w-8 animate-spin text-[#8fb3ff]"
-            aria-hidden="true"
-          />
-          <h1 className="mt-4 text-[18px] font-semibold tracking-normal 
text-[#f4f7fb]">正在加载工作台</h1>
-          <p className="mt-2 text-[13px] leading-6 text-[#9ca7ba]">
-            正在准备页面数据,请稍等。
-          </p>
-        </div>
-      </section>
+      <div className="h-px w-full overflow-hidden bg-[#11161d]">
+        <div
+          data-app-route-loading-indicator="true"
+          className="h-px w-1/3 animate-pulse bg-[#8fb3ff]"
+        />
+      </div>
     </main>
   );
 }
diff --git a/web-next/components/ui/cold-code-editor.test.tsx 
b/web-next/components/ui/cold-code-editor.test.tsx
index f6bbc08151..b20f4e1469 100644
--- a/web-next/components/ui/cold-code-editor.test.tsx
+++ b/web-next/components/ui/cold-code-editor.test.tsx
@@ -5,14 +5,15 @@ import { renderToStaticMarkup } from 'react-dom/server';
 import { describe, expect, it, vi } from 'vitest';
 
 vi.mock('@uiw/react-codemirror', () => ({
-  default: ({ value, readOnly, extensions, height, basicSetup, theme, 
onChange, ...props }: any) => (
+  default: ({ value, readOnly, editable, extensions, height, basicSetup, 
theme, onChange, ...props }: any) => (
     <div
       data-testid={props['data-testid']}
       data-mocked-codemirror="true"
       data-readonly={readOnly ? 'true' : 'false'}
+      data-editable={editable ? 'true' : 'false'}
       data-height={height}
       data-basic-setup={basicSetup ? 'true' : 'false'}
-      data-theme={theme ? 'custom-dark' : 'missing'}
+      data-theme={typeof theme === 'string' ? theme : theme ? 'custom-dark' : 
'missing'}
       data-extension-count={Array.isArray(extensions) ? 
String(extensions.length) : '0'}
       onClick={() => onChange?.(`${value}\nnext`)}
     >
@@ -51,8 +52,13 @@ describe('ColdCodeEditor', () => {
     expect(source).toContain("from '@codemirror/lang-json'");
     expect(source).toContain("from '@codemirror/lang-html'");
     expect(source).toContain('EditorView.lineWrapping');
-    expect(source).toContain('theme={oneDark}');
+    expect(source).toContain("theme={theme === 'vs-dark' ? oneDark : 
'light'}");
     expect(source).toContain('data-cold-code-editor="codemirror"');
+    expect(source).toContain('data-cold-code-editor-theme={theme}');
+    expect(source).toContain("data-cold-code-editor-loading={loading ? 'true' 
: 'false'}");
+    
expect(source).toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+    expect(source).toContain("data-cold-code-editor-folding={folding ? 'true' 
: 'false'}");
+    
expect(source).toContain("data-cold-code-editor-automatic-layout={automaticLayout
 ? 'true' : 'false'}");
     expect(source).toContain('data-cold-code-editor-license="codemirror-mit"');
     expect(source).not.toContain('<textarea');
 
@@ -69,10 +75,74 @@ describe('ColdCodeEditor', () => {
 
     expect(html).toContain('data-cold-code-editor="codemirror"');
     expect(html).toContain('data-cold-code-editor-language="yaml"');
+    expect(html).toContain('data-cold-code-editor-theme="vs-dark"');
+    expect(html).toContain('data-cold-code-editor-loading="false"');
+    
expect(html).toContain('data-cold-code-editor-loading-owner="cold-code-editor"');
+    expect(html).toContain('data-cold-code-editor-folding="true"');
+    expect(html).toContain('data-cold-code-editor-automatic-layout="true"');
     expect(html).toContain('data-cold-code-editor-readonly="true"');
     expect(html).toContain('data-cold-code-editor-license="codemirror-mit"');
     expect(html).toContain('data-mocked-codemirror="true"');
+    expect(html).toContain('data-editable="false"');
     expect(html).toContain('data-theme="custom-dark"');
     expect(html).toContain('apiVersion: hertzbeat/v1');
+    
expect(html).not.toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+  });
+
+  it('can expose the Angular light editor theme contract', async () => {
+    const { ColdCodeEditor } = await import('./cold-code-editor');
+    const html = renderToStaticMarkup(
+      <ColdCodeEditor
+        data-testid="cold-code-editor-light"
+        language="yaml"
+        theme="vs"
+        value="app: mysql"
+        onChange={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-code-editor-theme="vs"');
+    expect(html).toContain('data-theme="light"');
+  });
+
+  it('lets routes surface disabled folding and automatic layout contracts when 
needed', async () => {
+    const { ColdCodeEditor } = await import('./cold-code-editor');
+    const html = renderToStaticMarkup(
+      <ColdCodeEditor
+        data-testid="cold-code-editor-options"
+        language="yaml"
+        value="app: custom"
+        folding={false}
+        automaticLayout={false}
+        onChange={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-code-editor-folding="false"');
+    expect(html).toContain('data-cold-code-editor-automatic-layout="false"');
+  });
+
+  it('mirrors the Angular nz-code-editor loading contract', async () => {
+    const { ColdCodeEditor } = await import('./cold-code-editor');
+    const html = renderToStaticMarkup(
+      <ColdCodeEditor
+        data-testid="cold-code-editor-loading"
+        language="yaml"
+        value="app: loading"
+        loading
+        loadingLabel="Loading template YAML"
+        onChange={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-code-editor-loading="true"');
+    
expect(html).toContain('data-cold-code-editor-loading-owner="cold-code-editor"');
+    expect(html).toContain('aria-busy="true"');
+    expect(html).toContain('data-cold-code-editor-readonly="true"');
+    expect(html).toContain('data-readonly="true"');
+    expect(html).toContain('data-editable="false"');
+    
expect(html).toContain('data-cold-code-editor-loading-state="angular-nz-code-editor-loading"');
+    
expect(html).toContain('data-cold-code-editor-loading-state-owner="cold-code-editor"');
+    expect(html).toContain('Loading template YAML');
   });
 });
diff --git a/web-next/components/ui/cold-code-editor.tsx 
b/web-next/components/ui/cold-code-editor.tsx
index 0f33b69ea6..734139342f 100644
--- a/web-next/components/ui/cold-code-editor.tsx
+++ b/web-next/components/ui/cold-code-editor.tsx
@@ -14,12 +14,18 @@ import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
 export type ColdCodeEditorLanguage = 'yaml' | 'json' | 'html' | 'javascript' | 
'shell' | 'text';
+export type ColdCodeEditorTheme = 'vs' | 'vs-dark';
 
 export type ColdCodeEditorProps = Omit<React.HTMLAttributes<HTMLDivElement>, 
'onChange'> & {
   value: string;
   onChange?: (value: string) => void;
   language?: ColdCodeEditorLanguage;
+  theme?: ColdCodeEditorTheme;
   readOnly?: boolean;
+  loading?: boolean;
+  loadingLabel?: React.ReactNode;
+  folding?: boolean;
+  automaticLayout?: boolean;
   height?: string;
   minHeight?: string;
   placeholder?: string;
@@ -76,6 +82,55 @@ const coldCodeEditorTheme = EditorView.theme({
   dark: true
 });
 
+const coldCodeEditorLightTheme = EditorView.theme({
+  '&': {
+    backgroundColor: '#f8fafc',
+    color: '#111827',
+    border: '1px solid #cbd5e1',
+    borderRadius: '3px',
+    fontSize: '12px',
+    minHeight: 'inherit'
+  },
+  '&.cm-focused': {
+    borderColor: '#4e74f8',
+    outline: '2px solid rgba(78,116,248,0.14)'
+  },
+  '.cm-scroller': {
+    fontFamily:
+      'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation 
Mono", "Courier New", monospace',
+    lineHeight: '1.6',
+    minHeight: 'inherit'
+  },
+  '.cm-content': {
+    padding: '8px 0',
+    caretColor: '#1f2937',
+    minHeight: 'inherit'
+  },
+  '.cm-line': {
+    padding: '0 12px'
+  },
+  '.cm-gutters': {
+    backgroundColor: '#eef2f7',
+    borderRight: '1px solid #cbd5e1',
+    color: '#64748b'
+  },
+  '.cm-activeLine': {
+    backgroundColor: '#e8f0ff'
+  },
+  '.cm-activeLineGutter': {
+    backgroundColor: '#e8f0ff',
+    color: '#1f2937'
+  },
+  '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
+    backgroundColor: 'rgba(78,116,248,0.24)'
+  },
+  '.cm-placeholder': {
+    color: '#64748b'
+  }
+}, {
+  dark: false
+});
+
 function getLanguageExtension(language: ColdCodeEditorLanguage): Extension | 
null {
   switch (language) {
     case 'yaml':
@@ -97,7 +152,12 @@ export function ColdCodeEditor({
   value,
   onChange,
   language = 'text',
+  theme = 'vs-dark',
   readOnly = false,
+  loading = false,
+  loadingLabel = 'Loading editor',
+  folding = true,
+  automaticLayout = true,
   height,
   minHeight = '220px',
   placeholder,
@@ -109,23 +169,30 @@ export function ColdCodeEditor({
 }: ColdCodeEditorProps) {
   const extensions = useMemo(() => {
     const languageExtension = getLanguageExtension(language);
+    const themeExtension = theme === 'vs-dark' ? coldCodeEditorTheme : 
coldCodeEditorLightTheme;
     return [
       basicSetup,
-      coldCodeEditorTheme,
+      themeExtension,
       EditorView.lineWrapping,
       ...(languageExtension ? [languageExtension] : []),
-      ...(readOnly ? [EditorState.readOnly.of(true)] : [])
+      ...(readOnly || loading ? [EditorState.readOnly.of(true)] : [])
     ];
-  }, [language, readOnly]);
+  }, [language, loading, readOnly, theme]);
 
   return (
     <div
       {...props}
       data-cold-code-editor="codemirror"
       data-cold-code-editor-language={language}
-      data-cold-code-editor-readonly={readOnly ? 'true' : undefined}
+      data-cold-code-editor-theme={theme}
+      data-cold-code-editor-loading={loading ? 'true' : 'false'}
+      data-cold-code-editor-loading-owner="cold-code-editor"
+      data-cold-code-editor-folding={folding ? 'true' : 'false'}
+      data-cold-code-editor-automatic-layout={automaticLayout ? 'true' : 
'false'}
+      data-cold-code-editor-readonly={readOnly || loading ? 'true' : undefined}
       data-cold-code-editor-license="codemirror-mit"
-      className={cn('min-w-0 overflow-hidden rounded-[3px]', className)}
+      aria-busy={loading ? 'true' : undefined}
+      className={cn('relative min-w-0 overflow-hidden rounded-[3px]', 
className)}
       style={{ minHeight, ...style }}
     >
       {name ? <HiddenInput name={name} value={value} 
data-cold-code-editor-value="hidden" /> : null}
@@ -135,13 +202,22 @@ export function ColdCodeEditor({
         minHeight={minHeight}
         placeholder={placeholder}
         basicSetup={false}
-        theme={oneDark}
+        theme={theme === 'vs-dark' ? oneDark : 'light'}
         extensions={extensions}
-        readOnly={readOnly}
-        editable={!readOnly}
+        readOnly={readOnly || loading}
+        editable={!readOnly && !loading}
         aria-label={ariaLabel}
         onChange={nextValue => onChange?.(nextValue)}
       />
+      {loading ? (
+        <div
+          data-cold-code-editor-loading-state="angular-nz-code-editor-loading"
+          data-cold-code-editor-loading-state-owner="cold-code-editor"
+          className="absolute inset-0 flex items-center justify-center 
bg-[#0b0c0e]/72 text-[12px] font-semibold text-[#dbe4f0] backdrop-blur-[1px]"
+        >
+          {loadingLabel}
+        </div>
+      ) : null}
     </div>
   );
 }
diff --git a/web-next/components/ui/cold-confirm-dialog.test.tsx 
b/web-next/components/ui/cold-confirm-dialog.test.tsx
new file mode 100644
index 0000000000..b1a6ef5540
--- /dev/null
+++ b/web-next/components/ui/cold-confirm-dialog.test.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it } from 'vitest';
+
+import { ColdConfirmDialog } from './cold-confirm-dialog';
+
+describe('ColdConfirmDialog', () => {
+  it('renders shared confirmation chrome from runtime messages while 
preserving caller labels', () => {
+    const idleHtml = renderToStaticMarkup(
+      <ColdConfirmDialog
+        open
+        title="确认删除"
+        copy="删除后不可恢复。"
+        confirmLabel="确认"
+        cancelLabel="取消"
+        onCancel={() => undefined}
+        onConfirm={() => undefined}
+      />
+    );
+
+    
expect(idleHtml).toContain('data-cold-confirm-dialog="cold-confirm-dialog"');
+    expect(idleHtml).toContain('Confirm operation');
+    expect(idleHtml).toContain('确认删除');
+    expect(idleHtml).toContain('删除后不可恢复。');
+    expect(idleHtml).toContain('确认');
+    expect(idleHtml).toContain('取消');
+
+    const pendingHtml = renderToStaticMarkup(
+      <ColdConfirmDialog
+        open
+        title="确认删除"
+        copy="删除后不可恢复。"
+        confirmLabel="确认"
+        cancelLabel="取消"
+        pending
+        onCancel={() => undefined}
+        onConfirm={() => undefined}
+      />
+    );
+
+    expect(pendingHtml).toContain('Processing');
+    expect(pendingHtml).toContain('disabled=""');
+  });
+});
diff --git a/web-next/components/ui/cold-confirm-dialog.tsx 
b/web-next/components/ui/cold-confirm-dialog.tsx
index 0f33abb76a..abab8ef957 100644
--- a/web-next/components/ui/cold-confirm-dialog.tsx
+++ b/web-next/components/ui/cold-confirm-dialog.tsx
@@ -4,6 +4,11 @@ import React from 'react';
 import { AlertTriangle } from 'lucide-react';
 import { Button } from './button';
 import { OverlayDialog } from '../workbench/overlay-dialog';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
+
+function translateColdConfirm(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? 
SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
 
 type ColdConfirmDialogProps = {
   open: boolean;
@@ -30,7 +35,7 @@ export function ColdConfirmDialog({
     <OverlayDialog
       open={open}
       title={title}
-      kicker="确认操作"
+      kicker={translateColdConfirm('common.confirm.operation')}
       onClose={pending ? () => undefined : onCancel}
       maxWidthClassName="max-w-md"
       contentClassName="py-4"
@@ -40,7 +45,7 @@ export function ColdConfirmDialog({
             {cancelLabel}
           </Button>
           <Button size="sm" variant="primary" onClick={onConfirm} 
disabled={pending}>
-            {pending ? '处理中' : confirmLabel}
+            {pending ? translateColdConfirm('common.processing') : 
confirmLabel}
           </Button>
         </div>
       }
diff --git a/web-next/components/ui/date-time-range.test.tsx 
b/web-next/components/ui/date-time-range.test.tsx
index 6e9333e294..d4a298ca3b 100644
--- a/web-next/components/ui/date-time-range.test.tsx
+++ b/web-next/components/ui/date-time-range.test.tsx
@@ -118,4 +118,53 @@ describe('DateTimeRange', () => {
     expect(html).not.toContain('今天');
     expect(html).not.toContain('type="time"');
   });
+
+  it('renders English fallback default trigger labels', () => {
+    const html = renderToStaticMarkup(
+      <DateTimeRange
+        mode="time"
+        startName="periodStart"
+        endName="periodEnd"
+        startValue=""
+        endValue=""
+        onStartChange={vi.fn()}
+        onEndChange={vi.fn()}
+      />
+    );
+
+    expect(html.match(/>Not set</g)).toHaveLength(2);
+    expect(html).toContain('aria-label="Start"');
+    expect(html).toContain('aria-label="End"');
+    expect(html).not.toContain('未设置');
+    expect(html).not.toContain('aria-label="开始"');
+    expect(html).not.toContain('aria-label="结束"');
+  });
+
+  it('keeps empty-state picker chrome caller-owned', () => {
+    const html = renderToStaticMarkup(
+      <DateTimeRange
+        mode="time"
+        startName="periodStart"
+        endName="periodEnd"
+        startValue=""
+        endValue=""
+        onStartChange={vi.fn()}
+        onEndChange={vi.fn()}
+        startLabel="Start time"
+        endLabel="End time"
+        emptyLabel="Not set"
+        hourLabel="Hour"
+        minuteLabel="Minute"
+        previousMonthLabel="Previous month"
+        nextMonthLabel="Next month"
+        clearLabel="Clear"
+        confirmLabel="OK"
+      />
+    );
+
+    expect(html.match(/Not set/g)).toHaveLength(2);
+    expect(html).toContain('aria-label="Start time"');
+    expect(html).toContain('aria-label="End time"');
+    expect(html).not.toContain('未设置');
+  });
 });
diff --git a/web-next/components/ui/date-time-range.tsx 
b/web-next/components/ui/date-time-range.tsx
index e87b4a7fd7..15f057cbf1 100644
--- a/web-next/components/ui/date-time-range.tsx
+++ b/web-next/components/ui/date-time-range.tsx
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
 import ReactDatePicker, { registerLocale } from 'react-datepicker';
 import { zhCN } from 'date-fns/locale/zh-CN';
 import { ArrowRight } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -16,6 +17,13 @@ export interface DateTimeRangeProps extends 
React.HTMLAttributes<HTMLDivElement>
   onEndChange: (value: string) => void;
   startLabel?: string;
   endLabel?: string;
+  emptyLabel?: string;
+  hourLabel?: string;
+  minuteLabel?: string;
+  previousMonthLabel?: string;
+  nextMonthLabel?: string;
+  clearLabel?: string;
+  confirmLabel?: string;
   reserveActionSpace?: boolean;
 }
 
@@ -34,6 +42,10 @@ const TIME_MINUTES = Array.from({ length: 60 }, (_, minute) 
=> minute);
 
 registerLocale('zh-CN', zhCN);
 
+function translateDateTimeRange(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? 
SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 function pad2(value: number) {
   return String(value).padStart(2, '0');
 }
@@ -85,8 +97,8 @@ function formatDateChange(mode: DateTimeRangeProps['mode'], 
value: string, nextD
   return formatFromDate(mode, nextDate);
 }
 
-function formatValue(mode: DateTimeRangeProps['mode'], value: string) {
-  if (!value) return '未设置';
+function formatValue(mode: DateTimeRangeProps['mode'], value: string, 
emptyLabel: string) {
+  if (!value) return emptyLabel;
   return mode === 'time' ? value.slice(0, 5) : value.replace('T', ' ');
 }
 
@@ -188,11 +200,15 @@ function TimeColumn({
 function TimeColumns({
   mode,
   value,
-  onChange
+  onChange,
+  hourLabel,
+  minuteLabel
 }: {
   mode: DateTimeRangeProps['mode'];
   value: string;
   onChange: (value: string) => void;
+  hourLabel: string;
+  minuteLabel: string;
 }) {
   const { hour, minute } = getTimeParts(mode, value);
   return (
@@ -204,14 +220,14 @@ function TimeColumns({
       )}
     >
       <TimeColumn
-        label="时"
+        label={hourLabel}
         column="hour"
         values={TIME_HOURS}
         selectedValue={hour}
         onSelect={nextHour => onChange(formatWithTime(mode, value, nextHour, 
minute))}
       />
       <TimeColumn
-        label="分"
+        label={minuteLabel}
         column="minute"
         values={TIME_MINUTES}
         selectedValue={minute}
@@ -227,9 +243,29 @@ type ColdDateTimePickerProps = {
   value: string;
   label: string;
   onChange: (value: string) => void;
+  emptyLabel: string;
+  hourLabel: string;
+  minuteLabel: string;
+  previousMonthLabel: string;
+  nextMonthLabel: string;
+  clearLabel: string;
+  confirmLabel: string;
 };
 
-function ColdDateTimePicker({ mode, name, value, label, onChange }: 
ColdDateTimePickerProps) {
+function ColdDateTimePicker({
+  mode,
+  name,
+  value,
+  label,
+  onChange,
+  emptyLabel,
+  hourLabel,
+  minuteLabel,
+  previousMonthLabel,
+  nextMonthLabel,
+  clearLabel,
+  confirmLabel
+}: ColdDateTimePickerProps) {
   const triggerRef = React.useRef<HTMLButtonElement | null>(null);
   const [open, setOpen] = React.useState(false);
   const [draftValue, setDraftValue] = React.useState(value);
@@ -301,10 +337,10 @@ function ColdDateTimePicker({ mode, name, value, label, 
onChange }: ColdDateTime
               onChange={next => setDraftValue(formatDateChange(mode, 
draftValue, next))}
               inline
               locale="zh-CN"
-              previousMonthButtonLabel="上个月"
-              nextMonthButtonLabel="下个月"
-              previousMonthAriaLabel="上个月"
-              nextMonthAriaLabel="下个月"
+              previousMonthButtonLabel={previousMonthLabel}
+              nextMonthButtonLabel={nextMonthLabel}
+              previousMonthAriaLabel={previousMonthLabel}
+              nextMonthAriaLabel={nextMonthLabel}
               calendarClassName="hertzbeat-date-picker-calendar"
               shouldCloseOnSelect={false}
               dateFormat="yyyy-MM-dd HH:mm"
@@ -312,7 +348,13 @@ function ColdDateTimePicker({ mode, name, value, label, 
onChange }: ColdDateTime
             />
           </div>
         )}
-        <TimeColumns mode={mode} value={draftValue} onChange={setDraftValue} />
+        <TimeColumns
+          mode={mode}
+          value={draftValue}
+          onChange={setDraftValue}
+          hourLabel={hourLabel}
+          minuteLabel={minuteLabel}
+        />
       </div>
         <div data-cold-date-time-picker-panel="body-portal-clear-confirm" 
className="flex justify-end gap-2 border-t border-[#252b34] p-3">
           <button
@@ -321,7 +363,7 @@ function ColdDateTimePicker({ mode, name, value, label, 
onChange }: ColdDateTime
             className="h-7 min-w-[58px] rounded-[3px] border border-[#2b3039] 
bg-[#0d1015] px-2 text-[12px] font-semibold text-[#a9b0bb] 
hover:border-[#3b4454] hover:text-[#dbe4f0]"
             onClick={clearValue}
           >
-            清除
+            {clearLabel}
           </button>
           <button
             type="button"
@@ -329,7 +371,7 @@ function ColdDateTimePicker({ mode, name, value, label, 
onChange }: ColdDateTime
             className="h-7 min-w-[58px] rounded-[3px] border border-[#31405c] 
bg-[#182238] px-2 text-[12px] font-semibold text-[#d8e4ff] 
hover:border-[#4e74f8]"
             onClick={confirmValue}
           >
-            确定
+            {confirmLabel}
           </button>
         </div>
     </div>
@@ -344,7 +386,7 @@ function ColdDateTimePicker({ mode, name, value, label, 
onChange }: ColdDateTime
       <HiddenInput data-cold-date-time-picker-input="hidden-value" name={name} 
value={value} />
       <PickerTrigger
         ref={triggerRef}
-        displayValue={formatValue(mode, open ? draftValue : value)}
+        displayValue={formatValue(mode, open ? draftValue : value, emptyLabel)}
         empty={!(open ? draftValue : value)}
         expanded={open}
         aria-label={label}
@@ -363,8 +405,15 @@ export function DateTimeRange({
   endValue,
   onStartChange,
   onEndChange,
-  startLabel = '开始',
-  endLabel = '结束',
+  startLabel = translateDateTimeRange('time.range.start'),
+  endLabel = translateDateTimeRange('time.range.end'),
+  emptyLabel = translateDateTimeRange('time.range.unset'),
+  hourLabel = translateDateTimeRange('time.range.hour'),
+  minuteLabel = translateDateTimeRange('time.range.minute'),
+  previousMonthLabel = translateDateTimeRange('time.range.previous-month'),
+  nextMonthLabel = translateDateTimeRange('time.range.next-month'),
+  clearLabel = translateDateTimeRange('common.clear'),
+  confirmLabel = translateDateTimeRange('common.button.ok'),
   reserveActionSpace,
   className,
   ...props
@@ -392,6 +441,13 @@ export function DateTimeRange({
         value={startValue}
         label={startLabel}
         onChange={onStartChange}
+        emptyLabel={emptyLabel}
+        hourLabel={hourLabel}
+        minuteLabel={minuteLabel}
+        previousMonthLabel={previousMonthLabel}
+        nextMonthLabel={nextMonthLabel}
+        clearLabel={clearLabel}
+        confirmLabel={confirmLabel}
       />
       <ArrowRight className="mx-auto h-3.5 w-3.5 shrink-0 text-[#7e8494]" 
aria-hidden="true" />
       <ColdDateTimePicker
@@ -400,6 +456,13 @@ export function DateTimeRange({
         value={endValue}
         label={endLabel}
         onChange={onEndChange}
+        emptyLabel={emptyLabel}
+        hourLabel={hourLabel}
+        minuteLabel={minuteLabel}
+        previousMonthLabel={previousMonthLabel}
+        nextMonthLabel={nextMonthLabel}
+        clearLabel={clearLabel}
+        confirmLabel={confirmLabel}
       />
       {reserveActionSpace ? <span 
data-cold-date-time-range-reserved-action="true" aria-hidden="true" /> : null}
     </div>
diff --git a/web-next/components/ui/label-record-input.test.tsx 
b/web-next/components/ui/label-record-input.test.tsx
index 880e8f26e7..5925f3ed95 100644
--- a/web-next/components/ui/label-record-input.test.tsx
+++ b/web-next/components/ui/label-record-input.test.tsx
@@ -34,6 +34,11 @@ describe('LabelRecordInput', () => {
     
expect(html).toContain('data-cold-label-selector-value-input="searchable-value"');
     expect(html).toContain('data-cold-label-selector-value="hidden"');
     expect(html).toContain('type="hidden"');
+    expect(html).toContain('placeholder="Label key"');
+    expect(html).toContain('placeholder="Label value"');
+    expect(html).toContain('>Add<');
+    expect(html).toContain('>Remove<');
+    expect(html).toContain('aria-label="Remove service:checkout"');
     expect(html).toContain('rounded-[3px]');
     expect(html).not.toContain('data-cold-label-selector-chip-list="true"');
     expect(html).not.toContain('data-cold-label-selector-chip=');
diff --git a/web-next/components/ui/label-record-input.tsx 
b/web-next/components/ui/label-record-input.tsx
index ae2af381c1..3d1e6683e3 100644
--- a/web-next/components/ui/label-record-input.tsx
+++ b/web-next/components/ui/label-record-input.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
 import { Plus, X } from 'lucide-react';
 import type { AlertLabelOptions } from '../../lib/alert-label-options';
 import { DEFAULT_ALERT_LABEL_OPTIONS } from '../../lib/alert-label-options';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -54,6 +55,10 @@ function filterSuggestions(values: string[], query: string, 
limit = 8) {
     .slice(0, limit);
 }
 
+function translateLabelRecordInput(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? 
SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 export interface LabelRecordInputProps {
   value: string;
   onValueChange: (value: string) => void;
@@ -75,10 +80,10 @@ export const LabelRecordInput = 
React.forwardRef<HTMLInputElement, LabelRecordIn
       name,
       disabled,
       labelOptions = DEFAULT_ALERT_LABEL_OPTIONS,
-      keyPlaceholder = '标签名',
-      valuePlaceholder = '标签值',
-      addLabel = '添加',
-      removeLabel = '删除',
+      keyPlaceholder = translateLabelRecordInput('common.label.key'),
+      valuePlaceholder = translateLabelRecordInput('common.label.value'),
+      addLabel = translateLabelRecordInput('common.add'),
+      removeLabel = translateLabelRecordInput('common.remove'),
       containerClassName
     },
     ref
diff --git a/web-next/components/ui/number-stepper.test.tsx 
b/web-next/components/ui/number-stepper.test.tsx
index de34123e57..0be0be91cd 100644
--- a/web-next/components/ui/number-stepper.test.tsx
+++ b/web-next/components/ui/number-stepper.test.tsx
@@ -11,6 +11,8 @@ describe('NumberStepper', () => {
     expect(html).toContain('data-cold-number-stepper-input="true"');
     expect(html).toContain('data-cold-number-stepper-action="decrement"');
     expect(html).toContain('data-cold-number-stepper-action="increment"');
+    expect(html).toContain('>Decrease<');
+    expect(html).toContain('>Increase<');
     expect(html).toContain('inputMode="numeric"');
     expect(html).toContain('rounded-[3px]');
     expect(html).not.toContain('type="number"');
diff --git a/web-next/components/ui/number-stepper.tsx 
b/web-next/components/ui/number-stepper.tsx
index c44026afdc..c769372199 100644
--- a/web-next/components/ui/number-stepper.tsx
+++ b/web-next/components/ui/number-stepper.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import { Minus, Plus } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 
 function toNumber(value: string | number | undefined, fallback: number) {
@@ -18,6 +19,10 @@ function formatNumber(value: number) {
   return Number.isInteger(value) ? String(value) : 
String(Number(value.toFixed(6)));
 }
 
+function translateNumberStepper(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? 
SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 export interface NumberStepperProps extends 
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'value' | 
'onChange'> {
   value: string;
   onValueChange: (value: string) => void;
@@ -37,8 +42,8 @@ const NumberStepper = React.forwardRef<HTMLInputElement, 
NumberStepperProps>(
       step = 1,
       value,
       onValueChange,
-      decrementLabel = '减少',
-      incrementLabel = '增加',
+      decrementLabel = translateNumberStepper('common.decrement'),
+      incrementLabel = translateNumberStepper('common.increment'),
       ...props
     },
     ref
diff --git a/web-next/components/ui/search-row.test.tsx 
b/web-next/components/ui/search-row.test.tsx
index ae2fd67397..c71b588dd0 100644
--- a/web-next/components/ui/search-row.test.tsx
+++ b/web-next/components/ui/search-row.test.tsx
@@ -24,6 +24,9 @@ describe('SearchRow', () => {
     expect(html).toContain('data-cold-search-chrome="no-extra-input-shell"');
     expect(html).toContain('data-cold-search-action="submit"');
     expect(html).toContain('data-cold-search-action="clear"');
+    expect(html).toContain('<form');
+    expect(html).toContain('type="search"');
+    expect(html).toContain('type="submit"');
     expect(html).toContain('w-[320px]');
     expect(html).toContain('w-fit');
     expect(html).toContain('max-w-full');
@@ -54,4 +57,21 @@ describe('SearchRow', () => {
       html.indexOf('data-cold-search-action="submit"')
     );
   });
+
+  it('uses runtime defaults for action labels when callers omit shared copy', 
() => {
+    const html = renderToStaticMarkup(
+      <SearchRow
+        value="weekday"
+        placeholder="Policy name"
+        onValueChange={vi.fn()}
+        onSearch={vi.fn()}
+        onClear={vi.fn()}
+      />
+    );
+
+    expect(html).toContain('data-cold-search-action="submit"');
+    expect(html).toContain('data-cold-search-action="clear"');
+    expect(html).toContain('Search');
+    expect(html).toContain('Clear');
+  });
 });
diff --git a/web-next/components/ui/search-row.tsx 
b/web-next/components/ui/search-row.tsx
index 2a341e4c9f..300c839d39 100644
--- a/web-next/components/ui/search-row.tsx
+++ b/web-next/components/ui/search-row.tsx
@@ -2,11 +2,12 @@ import * as React from 'react';
 import { Search, X } from 'lucide-react';
 import { Button } from './button';
 import { cn } from '../../lib/utils';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 
 export interface SearchRowProps extends 
Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'onChange'> {
   value: string;
   placeholder: string;
-  searchLabel: string;
+  searchLabel?: string;
   clearLabel?: string;
   filters?: React.ReactNode;
   inputWidthClassName?: string;
@@ -20,12 +21,14 @@ export interface SearchRowProps extends 
Omit<React.FormHTMLAttributes<HTMLFormEl
 
 const searchButtonClassName =
   'h-8 min-w-[72px] rounded-[3px] border-[#2b3039] bg-[#101217] px-3 
text-[12px] font-semibold text-[#dbe4f0] hover:border-[#4e74f8] 
hover:bg-[#151b28] hover:text-white';
+const DEFAULT_SEARCH_ROW_SEARCH_LABEL = 
SUPPLEMENTAL_MESSAGES['en-US']?.['common.search'] ?? 'common.search';
+const DEFAULT_SEARCH_ROW_CLEAR_LABEL = 
SUPPLEMENTAL_MESSAGES['en-US']?.['common.clear'] ?? 'common.clear';
 
 export function SearchRow({
   value,
   placeholder,
-  searchLabel,
-  clearLabel,
+  searchLabel = DEFAULT_SEARCH_ROW_SEARCH_LABEL,
+  clearLabel = DEFAULT_SEARCH_ROW_CLEAR_LABEL,
   filters,
   inputWidthClassName = 'w-[320px]',
   searchDisabled = false,
diff --git a/web-next/components/ui/select.test.tsx 
b/web-next/components/ui/select.test.tsx
index 78aecac21a..314dc2d0e7 100644
--- a/web-next/components/ui/select.test.tsx
+++ b/web-next/components/ui/select.test.tsx
@@ -59,6 +59,31 @@ describe('Select', () => {
 
     expect(directNativeSelectFiles).toEqual([]);
   });
+
+  it('renders the catalog-backed empty trigger label when no option is 
available', () => {
+    const html = renderToStaticMarkup(<Select aria-label="empty select" />);
+
+    expect(html).toContain('data-cold-select-control="custom-trigger"');
+    expect(html).toContain('None');
+    expect(html).not.toContain('>-<');
+  });
+
+  it('can render the Angular searchable dropdown affordance for settings 
timezones', () => {
+    const html = renderToStaticMarkup(
+      <Select searchable searchPlaceholder="Search timezone" defaultOpen 
defaultValue="Asia/Shanghai" aria-label="Timezone">
+        <option value="Asia/Shanghai">Asia/Shanghai (+08:00) Shanghai</option>
+        <option value="UTC">UTC (+00:00) UTC</option>
+      </Select>
+    );
+    const source = readFileSync(resolve(process.cwd(), 
'components/ui/select.tsx'), 'utf8');
+
+    expect(html).toContain('data-cold-select-search="angular-nz-show-search"');
+    expect(html).toContain('type="search"');
+    expect(html).toContain('placeholder="Search timezone"');
+    expect(html).toContain('data-cold-select-listbox="custom-menu"');
+    expect(source).toContain('searchable?: boolean');
+    expect(source).toContain('data-cold-select-empty="search-empty"');
+  });
 });
 
 function walkSourceFiles(dir: string): string[] {
diff --git a/web-next/components/ui/select.tsx 
b/web-next/components/ui/select.tsx
index 16d4064239..6bdfe8317a 100644
--- a/web-next/components/ui/select.tsx
+++ b/web-next/components/ui/select.tsx
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { createPortal } from 'react-dom';
 import { ChevronDown } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -15,6 +16,8 @@ export const coldSelectIconClassName =
 const coldSelectListboxClassName =
   'fixed z-[90] min-w-[220px] overflow-y-auto rounded-[4px] border 
border-[#2b3039] bg-[#111318] p-1 text-[12px] font-semibold text-[#dbe4f0] 
shadow-[0_22px_60px_rgba(0,0,0,0.55)]';
 
+const DEFAULT_SELECT_EMPTY_LABEL = 
SUPPLEMENTAL_MESSAGES['en-US']?.['common.none'] ?? 'common.none';
+
 type SelectOption = {
   value: string;
   label: React.ReactNode;
@@ -24,6 +27,8 @@ type SelectOption = {
 export interface SelectProps extends 
React.SelectHTMLAttributes<HTMLSelectElement> {
   containerClassName?: string;
   defaultOpen?: boolean;
+  searchable?: boolean;
+  searchPlaceholder?: string;
   onValueChange?: (value: string) => void;
 }
 
@@ -73,6 +78,8 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
       id,
       'aria-label': ariaLabel,
       defaultOpen = false,
+      searchable = false,
+      searchPlaceholder,
       ...props
     },
     ref
@@ -82,6 +89,7 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
     const isControlled = value !== undefined;
     const [internalValue, setInternalValue] = 
React.useState(String(defaultValue ?? fallbackValue));
     const [open, setOpen] = React.useState(defaultOpen);
+    const [searchQuery, setSearchQuery] = React.useState('');
     const [listboxStyle, setListboxStyle] = 
React.useState<React.CSSProperties>({ position: 'fixed' });
     const shellRef = React.useRef<HTMLSpanElement>(null);
     const triggerRef = React.useRef<HTMLButtonElement>(null);
@@ -91,6 +99,10 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
     const listboxId = `${triggerId}-listbox`;
     const selectedValue = String(isControlled ? value : internalValue);
     const selectedOption = options.find(option => option.value === 
selectedValue) ?? options.find(option => !option.disabled);
+    const normalizedSearchQuery = searchQuery.trim().toLowerCase();
+    const visibleOptions = searchable && normalizedSearchQuery
+      ? options.filter(option => React.Children.toArray(option.label).join(' 
').toLowerCase().includes(normalizedSearchQuery))
+      : options;
 
     const updateListboxGeometry = React.useCallback(() => {
       if (typeof window === 'undefined' || !triggerRef.current) return;
@@ -134,6 +146,10 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
       return () => document.removeEventListener('pointerdown', onPointerDown);
     }, [open]);
 
+    React.useEffect(() => {
+      if (!open) setSearchQuery('');
+    }, [open]);
+
     React.useEffect(() => {
       if (!open || typeof window === 'undefined') return;
       window.addEventListener('resize', updateListboxGeometry);
@@ -172,7 +188,21 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
         className={cn(coldSelectListboxClassName)}
         style={listboxStyle}
       >
-        {options.map(option => {
+        {searchable ? (
+          <input
+            type="search"
+            value={searchQuery}
+            placeholder={searchPlaceholder}
+            aria-label={searchPlaceholder}
+            data-cold-select-search="angular-nz-show-search"
+            className="mb-1 h-8 w-full rounded-[3px] border border-[#2b3039] 
bg-[#0b0c0e] px-2.5 text-[12px] font-semibold text-[#dbe4f0] outline-none 
placeholder:text-[#858d9a] focus:border-[#4e74f8] focus:ring-2 
focus:ring-[rgba(78,116,248,0.12)]"
+            onChange={event => setSearchQuery(event.target.value)}
+            onKeyDown={event => {
+              if (event.key === 'Escape') setOpen(false);
+            }}
+          />
+        ) : null}
+        {visibleOptions.map(option => {
           const selected = option.value === selectedOption?.value;
           return (
             <button
@@ -194,6 +224,11 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
             </button>
           );
         })}
+        {visibleOptions.length === 0 ? (
+          <div data-cold-select-empty="search-empty" className="px-2.5 py-2 
text-[12px] text-[#8f99ab]">
+            {DEFAULT_SELECT_EMPTY_LABEL}
+          </div>
+        ) : null}
       </div>
     ) : null;
 
@@ -257,7 +292,7 @@ const Select = React.forwardRef<HTMLSelectElement, 
SelectProps>(
             }
           }}
         >
-          <span className="min-w-0 truncate">{selectedOption?.label ?? 
'-'}</span>
+          <span className="min-w-0 truncate">{selectedOption?.label ?? 
DEFAULT_SELECT_EMPTY_LABEL}</span>
         </button>
         <ChevronDown data-cold-select-icon="chevron" 
className={cn(coldSelectIconClassName, open ? 'text-[#dbe4f0]' : null)} 
aria-hidden="true" />
         {listbox ? (canUsePortal ? createPortal(listbox, document.body) : 
listbox) : null}
diff --git a/web-next/components/ui/tag-input.test.tsx 
b/web-next/components/ui/tag-input.test.tsx
index 3fbc927fdb..f9a4d4871e 100644
--- a/web-next/components/ui/tag-input.test.tsx
+++ b/web-next/components/ui/tag-input.test.tsx
@@ -27,6 +27,9 @@ describe('TagInput', () => {
     expect(html).toContain('data-cold-tag-input-control="draft"');
     expect(html).toContain('data-cold-tag-input-value="hidden"');
     expect(html).toContain('type="hidden"');
+    expect(html).toContain('placeholder="Add tag"');
+    expect(html).toContain('aria-label="Remove alertname"');
+    expect(html).toContain('aria-label="Remove service"');
     expect(html).toContain('rounded-[3px]');
     expect(html).not.toContain('data-cold-tag-suggestion=');
     expect(html).not.toContain('data-cold-tag-suggestions-owner=');
diff --git a/web-next/components/ui/tag-input.tsx 
b/web-next/components/ui/tag-input.tsx
index e0eadcde2d..ebb31b04eb 100644
--- a/web-next/components/ui/tag-input.tsx
+++ b/web-next/components/ui/tag-input.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import { X } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 import { HiddenInput } from './hidden-input';
 
@@ -20,6 +21,10 @@ type PopoverMetrics = {
   width: number;
 };
 
+function translateTagInput(key: string) {
+  return SUPPLEMENTAL_MESSAGES['en-US']?.[key] ?? 
SUPPLEMENTAL_MESSAGES['zh-CN']?.[key] ?? key;
+}
+
 export interface TagInputProps extends 
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
   value: string;
   onValueChange: (value: string) => void;
@@ -118,7 +123,7 @@ const TagInput = React.forwardRef<HTMLInputElement, 
TagInputProps>(
               disabled={disabled}
               data-cold-tag-remove={tag}
               className="grid h-4 w-4 place-items-center rounded-[2px] 
text-[#8f99ab] transition hover:bg-[#202838] hover:text-[#f5f7fb] 
disabled:pointer-events-none"
-              aria-label={`删除 ${tag}`}
+              aria-label={`${translateTagInput('common.remove')} ${tag}`}
               onClick={() => removeTag(tag)}
             >
               <X className="h-3 w-3" aria-hidden="true" />
@@ -135,7 +140,7 @@ const TagInput = React.forwardRef<HTMLInputElement, 
TagInputProps>(
             'h-6 min-w-[120px] flex-1 border-0 bg-transparent px-1 text-[12px] 
font-semibold text-[#dbe4f0] outline-none placeholder:text-[#858d9a]',
             className
           )}
-          placeholder={tags.length === 0 ? placeholder : '添加标签'}
+          placeholder={tags.length === 0 ? placeholder : 
translateTagInput('common.tag.add')}
           onChange={event => {
             setDraft(event.target.value);
             positionPopover(event.currentTarget);
diff --git a/web-next/components/workbench/overlay-dialog.test.tsx 
b/web-next/components/workbench/overlay-dialog.test.tsx
index e1245bc632..9361d23e41 100644
--- a/web-next/components/workbench/overlay-dialog.test.tsx
+++ b/web-next/components/workbench/overlay-dialog.test.tsx
@@ -39,6 +39,7 @@ describe('overlay dialog', () => {
 
     const overlay = container.querySelector('[data-overlay-dialog="true"]') as 
HTMLElement | null;
     expect(overlay).not.toBeNull();
+    
expect(overlay?.getAttribute('data-overlay-dialog-mask-closable')).toBe('true');
 
     await act(async () => {
       overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
@@ -86,6 +87,37 @@ describe('overlay dialog', () => {
 
     const overlay = container.querySelector('[data-overlay-dialog="true"]') as 
HTMLElement | null;
     expect(overlay).not.toBeNull();
+    
expect(overlay?.getAttribute('data-overlay-dialog-mask-closable')).toBe('false');
+
+    await act(async () => {
+      overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+      await Promise.resolve();
+    });
+
+    expect(onClose).not.toHaveBeenCalled();
+  });
+
+  it('allows centered dialogs to explicitly preserve non-click-away modal 
semantics with route markers', async () => {
+    const onClose = vi.fn();
+
+    await act(async () => {
+      root.render(
+        <OverlayDialog
+          open={true}
+          title="标签管理"
+          onClose={onClose}
+          maskClosable={false}
+          overlayProps={{ 'data-label-dialog-mask-closable': 'false' }}
+        >
+          <input aria-label="标签名称" />
+        </OverlayDialog>
+      );
+      await Promise.resolve();
+    });
+
+    const overlay = container.querySelector('[data-overlay-dialog="true"]') as 
HTMLElement | null;
+    
expect(overlay?.getAttribute('data-overlay-dialog-mask-closable')).toBe('false');
+    
expect(overlay?.getAttribute('data-label-dialog-mask-closable')).toBe('false');
 
     await act(async () => {
       overlay?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
diff --git a/web-next/components/workbench/overlay-dialog.tsx 
b/web-next/components/workbench/overlay-dialog.tsx
index 780e27eeb5..3e5be5d21f 100644
--- a/web-next/components/workbench/overlay-dialog.tsx
+++ b/web-next/components/workbench/overlay-dialog.tsx
@@ -2,20 +2,12 @@
 
 import React from 'react';
 import { X } from 'lucide-react';
+import { SUPPLEMENTAL_MESSAGES } from '../../lib/i18n-runtime-messages';
 import { cn } from '../../lib/utils';
 
-export function OverlayDialog({
-  open,
-  title,
-  kicker,
-  footer,
-  onClose,
-  children,
-  className,
-  contentClassName,
-  maxWidthClassName = 'max-w-5xl',
-  placement = 'center'
-}: {
+const DEFAULT_OVERLAY_DIALOG_CLOSE_LABEL = 
SUPPLEMENTAL_MESSAGES['en-US']?.['common.dialog.close'] ?? 
'common.dialog.close';
+
+type OverlayDialogProps = {
   open: boolean;
   title: React.ReactNode;
   kicker?: React.ReactNode;
@@ -26,19 +18,40 @@ export function OverlayDialog({
   contentClassName?: string;
   maxWidthClassName?: string;
   placement?: 'center' | 'right';
-}) {
+  closeLabel?: string;
+  maskClosable?: boolean;
+  overlayProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 
'className' | 'onClick' | 'title'>;
+};
+
+export function OverlayDialog({
+  open,
+  title,
+  kicker,
+  footer,
+  onClose,
+  children,
+  className,
+  contentClassName,
+  maxWidthClassName = 'max-w-5xl',
+  placement = 'center',
+  closeLabel = DEFAULT_OVERLAY_DIALOG_CLOSE_LABEL,
+  maskClosable,
+  overlayProps
+}: OverlayDialogProps) {
   if (!open) {
     return null;
   }
   const isSideDrawer = placement === 'right';
+  const isMaskClosable = maskClosable ?? isSideDrawer;
   const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
-    if (isSideDrawer && event.target === event.currentTarget) {
+    if (isMaskClosable && event.target === event.currentTarget) {
       onClose();
     }
   };
 
   return (
     <div
+      {...overlayProps}
       className={cn(
         'fixed inset-0 z-50 flex bg-[rgba(11,12,14,0.88)]',
         isSideDrawer ? 'items-stretch justify-end p-0' : 'items-center 
justify-center p-4'
@@ -48,6 +61,7 @@ export function OverlayDialog({
       aria-label={typeof title === 'string' ? title : undefined}
       data-overlay-dialog="true"
       data-overlay-dialog-placement={placement}
+      data-overlay-dialog-mask-closable={isMaskClosable ? 'true' : 'false'}
       onClick={handleOverlayClick}
     >
       <div
@@ -67,7 +81,7 @@ export function OverlayDialog({
           <button
             type="button"
             onClick={onClose}
-            aria-label="Close dialog"
+            aria-label={closeLabel}
             className="inline-flex h-8 w-8 items-center justify-center 
rounded-[2px] border border-[var(--ops-border-color)] bg-transparent 
text-[var(--ops-text-secondary)] transition hover:border-[var(--ops-primary)] 
hover:bg-[var(--ops-surface-raised)] hover:text-[var(--ops-text-primary)]"
           >
             <X size={16} />
diff --git a/web-next/components/workbench/workspace-tab-strip.test.tsx 
b/web-next/components/workbench/workspace-tab-strip.test.tsx
index 74b6d9f4c4..6748e11664 100644
--- a/web-next/components/workbench/workspace-tab-strip.test.tsx
+++ b/web-next/components/workbench/workspace-tab-strip.test.tsx
@@ -25,6 +25,7 @@ describe('workspace tab strip', () => {
 
     expect(html).toContain('Summary');
     expect(html).toContain('Details');
+    expect(html).toContain('aria-label="Workspace navigation"');
     expect(html).toContain('border-[var(--ops-border-color)]');
     expect(html).toContain('bg-[var(--ops-surface-panel)]');
     expect(html).toContain('bg-[var(--ops-surface-raised)]');
@@ -38,6 +39,7 @@ describe('workspace tab strip', () => {
   it('renders shared default tabs with ops-shell chrome and badge treatment', 
() => {
     const html = renderToStaticMarkup(
       <WorkspaceTabStrip
+        ariaLabel="Custom workspace sections"
         tabs={[
           { key: 'all', label: 'All', active: true, badge: '4' },
           { key: 'errors', label: 'Errors', onSelect: () => {} }
@@ -48,6 +50,7 @@ describe('workspace tab strip', () => {
     expect(html).toContain('All');
     expect(html).toContain('Errors');
     expect(html).toContain('>4<');
+    expect(html).toContain('aria-label="Custom workspace sections"');
     expect(html).toContain('border-[var(--ops-primary)]');
     expect(html).toContain('bg-[var(--ops-surface-raised)]');
     expect(html).toContain('text-[var(--ops-text-tertiary)]');


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to