diff --git a/admin-dashboard/README.md b/admin-dashboard/README.md index bfe5e3a..5f04806 100644 --- a/admin-dashboard/README.md +++ b/admin-dashboard/README.md @@ -2,7 +2,7 @@ waitqueue.js 的轻量实时运维控制台。它直接读取后端队列配置、Redis 运行快照和当前进程计数,不使用 mock 数据,也不展示当前系统无法证明的历史趋势。 -界面遵循 [fullstack-ai-infra/design-system@9d048faa](https://github.com/fullstack-ai-infra/design-system/tree/9d048faaabe0429a6a8720bfbb31418544237b6b) 的 **Warm Agent Workspace** 视觉契约:暖象牙画布、stone 导航、paper 表面、charcoal 文字和 sage 主操作。布局采用 72px 模块栏、256px 队列上下文栏和 60px 顶栏;表格、表单、抽屉、弹窗及反馈继续由 Ant Design 6 提供。 +界面遵循 [fullstack-ai-infra/design-system@9d048faa](https://github.com/fullstack-ai-infra/design-system/tree/9d048faaabe0429a6a8720bfbb31418544237b6b) 的 **Warm Agent Workspace** 视觉契约:暖象牙画布、stone 导航、paper 表面、charcoal 文字和 sage 主操作。布局采用 72px 模块栏、256px 队列上下文栏和 60px 顶栏;模块与队列导航使用 Ant Design `Menu`,指标和响应式栅格使用 `Row / Col / Card / Statistic`,结构化数据使用 `Table / Descriptions`,表单、抽屉、弹窗及反馈也统一由 Ant Design 6 提供。 设计系统当前尚未发布稳定 npm 版本,且其 React 18 peer contract 与本项目 React 19 不兼容,因此这里使用轻量 semantic adapter 将上游 token 映射到 Ant Design `ConfigProvider`,不引入重复的 Radix、Tailwind 或状态管理运行时。待设计系统正式支持 React 19 后,可直接切换到包依赖。 @@ -15,7 +15,7 @@ waitqueue.js 的轻量实时运维控制台。它直接读取后端队列配置 3. **死信**:选择队列后分页查看 DLQ;重放前必须二次确认,后端用 `entryId` 校验 generation,避免把已更新的旧记录误重放。 4. **诊断**:分别查看进程 liveness、MySQL/Redis readiness,并核对 Prometheus 抓取契约。 -页面每 10 秒自动刷新,浏览器切到后台时暂停;手动刷新会同时更新队列快照和健康状态。顶栏只有在 liveness 与 readiness 都成功时才显示“服务在线”,依赖异常会显示“依赖异常”。桌面端使用模块栏、固定队列目录和表格,窄屏隐藏目录并通过抽屉选择队列,表格自动切换为卡片;浅色/深色主题共用同一套语义 token。 +页面每 10 秒自动刷新,浏览器切到后台时暂停;手动刷新会同时更新队列快照和健康状态。顶栏只保留服务状态、更新时间和刷新等全局工具;“提交任务”只在明确选中队列后的详情页出现,避免误投。桌面端使用模块栏、固定队列目录和表格,窄屏隐藏目录并通过抽屉选择队列,表格自动切换为卡片;浅色/深色主题共用同一套语义 token。 当前没有任务历史存储,因此页面不会展示吞吐趋势、成功率、平均耗时或任务明细。 @@ -104,7 +104,7 @@ Browser ## 技术与目录 -运行时直接依赖只有 Next.js、React、Ant Design、Lucide 图标与 Ant SSR 样式运行时;没有 Ant Design Pro、Redux、Axios、Mock.js、Tailwind、Radix 或图表库。Lucide 与设计系统的图标契约一致,并按组件 tree-shaking。Pages Router 使用 `_document.tsx` 提取 Ant Design CSS-in-JS 样式,避免首屏闪烁;浅色与深色使用独立的 Ant CSS variable scope,避免服务端浅色变量覆盖客户端深色状态。Next 16 的开发与生产构建显式使用 Webpack,以确保 Ant Design 与提取器共享同一个样式上下文;CI 会检查真实 Ant 组件规则、主题隔离和关键暗色文字对比度。 +运行时直接依赖只有 Next.js、React、Ant Design、Lucide 图标与 Ant SSR 样式运行时;没有 Ant Design Pro、Redux、Axios、Mock.js、Tailwind、Radix 或图表库。Lucide 与设计系统的图标契约一致,并按组件 tree-shaking。Pages Router 使用 `_document.tsx` 提取 Ant Design CSS-in-JS 样式,避免首屏闪烁;浅色与深色使用独立的 Ant CSS variable scope,避免服务端浅色变量覆盖客户端深色状态。组件颜色、状态和密度由 `ConfigProvider` Design Token 控制,CSS Module 只负责产品布局、信息密度和响应式,不使用 `!important` 覆盖 Ant 内部样式。Next 16 的开发与生产构建显式使用 Webpack,以确保 Ant Design 与提取器共享同一个样式上下文;CI 会检查真实 Ant 组件规则、CSS Module 引用、主题隔离和关键暗色文字对比度。 ```text admin-dashboard/ diff --git a/admin-dashboard/scripts/check-design-contract.mjs b/admin-dashboard/scripts/check-design-contract.mjs index 1377653..2c1f7b6 100644 --- a/admin-dashboard/scripts/check-design-contract.mjs +++ b/admin-dashboard/scripts/check-design-contract.mjs @@ -32,6 +32,7 @@ expectText(documentSource, "localStorage.getItem('waitqueue-theme')", 'legacy th expectText(documentSource, "dataset.themeReady=mode==='dark'?'false':'true'", 'dark first-paint guard'); expectText(globalCss, "[data-theme-ready='false']", 'dark first-paint CSS guard'); expectText(theme, 'colorTextPlaceholder: supportingText', 'accessible placeholder mapping'); +expectText(theme, 'colorTextTertiary: supportingText', 'accessible tertiary text mapping'); expectText(theme, 'cssVar: { key: `waitqueue-${mode}` }', 'isolated light and dark Ant variable scopes'); expectText(theme, 'defaultColor: colors.foreground', 'readable Ant button mapping'); expectText(theme, 'itemColor: colors.foregroundMuted', 'readable Ant navigation mapping'); @@ -74,9 +75,34 @@ if (contrast(darkToken('--ui-control-border'), darkSurface) < 3) { fail('dark control boundary contrast is below 3'); } +const themeBlock = (mode) => theme.match(new RegExp(`${mode}: \\{([\\s\\S]*?)\\n \\},`))?.[1]; +for (const mode of ['light', 'dark']) { + const block = themeBlock(mode); + if (!block) fail(`${mode} theme palette is missing`); + const color = (role) => { + const value = block.match(new RegExp(`${role}:\\s*'(#[\\da-f]{6})'`, 'i'))?.[1]; + if (!value) fail(`${mode} theme palette is missing ${role}`); + return value; + }; + for (const role of ['info', 'success', 'warning', 'danger']) { + const ratio = contrast(color(role), color(`${role}Soft`)); + if (ratio < 4.5) fail(`${mode} ${role} tag contrast ${ratio.toFixed(2)} is below 4.5`); + } +} + if (/#[\da-f]{3,8}\b/i.test(dashboardCss) || /#[\da-f]{3,8}\b/i.test(page)) { fail('product layout must use semantic variables instead of raw palette values'); } +if (dashboardCss.includes('!important')) { + fail('product layout must not override Ant Design component tokens with !important'); +} + +const referencedStyles = new Set([...page.matchAll(/styles\.([A-Za-z][A-Za-z0-9_]*)/g)].map((match) => match[1])); +for (const styleName of referencedStyles) { + if (!new RegExp(`\\.${styleName}(?=[\\s,{:.>])`).test(dashboardCss)) { + fail(`CSS module class referenced by the page is missing: ${styleName}`); + } +} for (const legacy of ['WaitQueue Workbench', 'Runtime Pulse', 'Delivery & Recovery', '#00c98b', '#17181b']) { if (page.includes(legacy) || dashboardCss.includes(legacy)) fail(`legacy visual language remains: ${legacy}`); @@ -86,6 +112,13 @@ expectText(page, 'data-product="waitqueue-console"', 'stable dashboard marker'); expectText(page, 'aria-label="主模块导航"', 'primary navigation landmark'); expectText(page, 'aria-label="队列目录"', 'queue catalog landmark'); expectText(page, 'aria-label="运行模块"', 'rail menu name'); +expectText(page, 'className={styles.queueMenu}', 'Ant Design queue menu'); +expectText(page, ' = { diagnostics: { title: '服务诊断', description: '探针与指标契约' }, }; +const VIEW_MENU_ITEMS = [ + { key: 'overview', icon: , label: '总览' }, + { key: 'queues', icon: , label: '队列' }, + { key: 'deadLetters', icon: , label: '死信' }, + { key: 'diagnostics', icon: , label: '诊断' }, +] satisfies Array<{ key: ViewKey; icon: ReactNode; label: string }>; + +const RAIL_MENU_STYLES: MenuProps['styles'] = { + root: { width: '100%', flex: 1, borderInlineEnd: 0 }, + item: { + display: 'flex', + width: 'var(--ui-rail-item-width, 52px)', + height: 'var(--ui-rail-item-height, 56px)', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 3, + margin: '4px 2px', + padding: '5px 2px', + lineHeight: 1, + }, + itemIcon: { display: 'inline-grid', minWidth: 19, margin: 0, placeItems: 'center' }, + itemContent: { width: '100%', margin: 0, overflow: 'visible', textAlign: 'center', textOverflow: 'clip' }, +}; + +const QUEUE_MENU_STYLES: MenuProps['styles'] = { + root: { background: 'transparent', borderInlineEnd: 0 }, + item: { height: 62, marginBlock: 2, marginInline: 0, paddingInline: 10, lineHeight: 'normal' }, + itemIcon: { lineHeight: 1 }, + itemContent: { minWidth: 0 }, +}; + async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit): Promise { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); @@ -283,6 +320,33 @@ function queueState(queue: QueueOverviewItem): { label: string; color: 'success' return { label: '空闲', color: 'default', badge: 'default' }; } +type StatusTone = 'success' | 'warning' | 'error' | 'processing' | 'default'; + +const STATUS_TAG_STYLES: Record, TagProps['styles']> = { + success: { root: { color: 'var(--ui-success)', background: 'var(--ui-success-soft)' } }, + warning: { root: { color: 'var(--ui-warning)', background: 'var(--ui-warning-soft)' } }, + error: { root: { color: 'var(--ui-danger)', background: 'var(--ui-danger-soft)' } }, + processing: { root: { color: 'var(--ui-info)', background: 'var(--ui-info-soft)' } }, +}; + +const QUEUE_COUNT_BADGE_STYLES: BadgeProps['styles'] = { + indicator: { color: 'var(--ui-primary-foreground)', background: 'var(--ui-primary)' }, +}; + +function StatusTag({ tone, icon, children, label }: { tone: StatusTone; icon?: ReactNode; children: ReactNode; label?: string }) { + return ( + + {children} + + ); +} + function serviceState(overview: QueueOverview | null, error: string, health: HealthState, loading: boolean): ServiceState { if (loading && !overview) return 'SYNCING'; if (!overview) return 'OFFLINE'; @@ -294,7 +358,7 @@ function serviceState(overview: QueueOverview | null, error: string, health: Hea } function stateTag(state: ServiceState) { - const map: Record = { + const map: Record = { SYNCING: { color: 'processing', icon: , text: '正在同步' }, ONLINE: { color: 'success', icon: , text: '服务在线' }, DEGRADED: { color: 'warning', icon: , text: '依赖异常' }, @@ -302,17 +366,15 @@ function stateTag(state: ServiceState) { OFFLINE: { color: 'error', icon: , text: '服务离线' }, }; const item = map[state]; - return {item.text}; + return {item.text}; } function ConsolePageHeader({ - eyebrow, title, description, meta, actions, }: { - eyebrow: string; title: ReactNode; description: ReactNode; meta?: ReactNode; @@ -321,9 +383,8 @@ function ConsolePageHeader({ return (
-
{eyebrow}
-

{title}

-

{description}

+ {title} + {description} {meta &&
{meta}
}
{actions &&
{actions}
} @@ -331,6 +392,48 @@ function ConsolePageHeader({ ); } +function SummaryMetricCard({ + title, + icon, + value, + suffix, + tone = 'default', + progress, + footer, +}: { + title: string; + icon: ReactNode; + value: number | string; + suffix?: ReactNode; + tone?: 'default' | 'warning' | 'danger'; + progress?: number; + footer?: ReactNode; +}) { + const toneClass = tone === 'danger' + ? styles.metricDanger + : tone === 'warning' + ? styles.metricWarning + : ''; + return ( + + + {title} + {icon} + + )} + value={value} + suffix={suffix} + /> + {progress !== undefined && ( + + )} + {footer &&
{footer}
} +
+ ); +} + function SidebarCatalog({ overview, queues, @@ -348,6 +451,34 @@ function SidebarCatalog({ onSelect: (queueId: number) => void; onCreate: () => void; }) { + const queueItems: MenuProps['items'] = queues.map((queue) => { + const state = queueState(queue); + const avatarClass = state.badge === 'error' + ? styles.queueAvatarDanger + : state.badge === 'warning' + ? styles.queueAvatarWarning + : state.badge === 'success' + ? styles.queueAvatarHealthy + : styles.queueAvatarNeutral; + return { + key: String(queue.queueId), + icon: , + label: ( + + + {queue.namespace} + Q-{String(queue.queueId).padStart(3, '0')} · {state.label} + + + {queue.waiting} + 等待 + + {queue.queueId === activeQueueId && 当前队列} + + ), + }; + }); + return (
@@ -356,7 +487,7 @@ function SidebarCatalog({ WaitQueue 队列运行中心 - +
@@ -376,42 +507,28 @@ function SidebarCatalog({
- 每 10 秒自动刷新 + 每 10 秒自动刷新
); @@ -656,12 +773,10 @@ const Dashboard: NextPage = () => { setMobileCatalogOpen(false); }; - const menuItems: MenuProps['items'] = [ - { key: 'overview', icon: , label: '总览' }, - { key: 'queues', icon: , label: '队列' }, - { key: 'deadLetters', icon: , label: '死信' }, - { key: 'diagnostics', icon: , label: '诊断' }, - ]; + const menuItems: MenuProps['items'] = VIEW_MENU_ITEMS.map((item) => ({ + ...item, + label: {item.label}{view === item.key && ,当前页面}, + })); const queueColumns: TableColumnsType = [ { @@ -670,7 +785,7 @@ const Dashboard: NextPage = () => { width: 230, render: (_, queue) => (
- + Q-{String(queue.queueId).padStart(3, '0')} · {displayHook(queue.hookUrl)}
), @@ -681,7 +796,7 @@ const Dashboard: NextPage = () => { width: 110, render: (_, queue) => { const state = queueState(queue); - return {state.label}; + return {state.label}; }, }, { @@ -717,28 +832,21 @@ const Dashboard: NextPage = () => { ), }, - { - title: '', - key: 'actions', - width: 80, - align: 'right', - render: (_, queue) => , - }, ]; const deadLetterColumns: TableColumnsType = [ { title: '任务 ID', dataIndex: 'taskId', ellipsis: true, render: (value: string) => {value} }, - { title: '失败原因', dataIndex: 'reason', width: 150, render: (value: DeadLetterItem['reason']) => {value === 'lease_expired' ? '租约过期' : '回调失败'} }, + { title: '失败原因', dataIndex: 'reason', width: 150, render: (value: DeadLetterItem['reason']) => {value === 'lease_expired' ? '租约过期' : '回调失败'} }, { title: '重试次数', dataIndex: 'retryCount', width: 110, align: 'right' }, { title: '失败时间', dataIndex: 'failedAt', width: 180, render: formatTimestamp }, - { title: '操作', key: 'action', width: 110, fixed: 'right', render: (_, item) => }, + { title: '操作', key: 'action', width: 110, fixed: 'right', render: (_, item) => }, ]; const catalog = ( { @@ -756,53 +864,72 @@ const Dashboard: NextPage = () => { const renderOverview = () => ( <> 已注册 {overview?.summary.queueCount ?? 0} 个队列 · 每 10 秒自动同步} actions={} /> -
- 0 ? styles.metricWarning : ''}`}> -
等待任务
- -

最老等待:{formatAge(overview?.summary.oldestWaitingAgeSeconds, overview?.summary.waiting)}

-
- -
运行容量
- - -
- 0 ? styles.metricWarning : ''}`}> -
延迟重试
- -

等待下一次回队调度

-
- 0 ? styles.metricDanger : ''}`}> -
死信任务
- - -
-
- -
- 队列健康查看实时积压和失败状态} - extra={{overview?.summary.queueCount ?? 0} 个队列} - > + + + } + value={overview?.summary.waiting ?? '—'} + suffix="个" + tone={(overview?.summary.waiting ?? 0) > 0 ? 'warning' : 'default'} + footer={<>最老等待:{formatAge(overview?.summary.oldestWaitingAgeSeconds, overview?.summary.waiting)}} + /> + + + } + value={overview ? `${overview.summary.running} / ${overview.summary.capacity}` : '—'} + progress={overview?.summary.utilization ?? 0} + footer={<>容量利用率 {overview?.summary.utilization ?? 0}%} + /> + + + } + value={metric(overview?.summary.retrying)} + suffix="个" + tone={(overview?.summary.retrying ?? 0) > 0 ? 'warning' : 'default'} + footer="等待下一次回队调度" + /> + + + } + value={metric(overview?.summary.deadLetters)} + suffix="个" + tone={(overview?.summary.deadLetters ?? 0) > 0 ? 'danger' : 'default'} + footer={} + /> + + + + + + 队列健康查看实时积压和失败状态} + extra={{overview?.summary.queueCount ?? 0} 个队列} + > {(overview?.queues.length ?? 0) > 0 ? ( <>
- +
{(overview?.queues ?? []).map((queue) => { const state = queueState(queue); return ( )} - - - 投递与恢复当前进程累计值
} - > -
-
回调成功{metric(overview?.summary.callbackSuccesses)}
-
回调失败 0 ? styles.dangerText : undefined}>{metric(overview?.summary.callbackFailures)}
-
成功认领{metric(overview?.summary.claims)}
-
租约恢复{metric(overview?.summary.recovered)}
-
-
- - 统计自 {formatTimestamp(overview?.metricsStartedAt)},服务重启后重新计数。 -
- - + + + + + 投递与恢复当前进程累计值} + > + + + 0 ? styles.dangerStatistic : undefined} title="回调失败" value={metric(overview?.summary.callbackFailures)} /> + + + +
+ + 统计自 {formatTimestamp(overview?.metricsStartedAt)},服务重启后重新计数。 +
+ + + ); const renderQueueDetail = () => activeQueue ? ( <> - {queueState(activeQueue).label} + {queueState(activeQueue).label} 配置更新于 {formatTimestamp(activeQueue.updatedAt)} } - actions={} + actions={( + <> + + + + )} /> -
- 0 ? styles.metricWarning : ''}`}> -
等待任务
- -

最老等待:{formatAge(activeQueue.oldestWaitingAgeSeconds, activeQueue.waiting)}

-
- -
运行中
- -

当前可用槽位:{activeQueue.available}

-
- 0 ? styles.metricWarning : ''}`}> -
延迟重试
- -

失败后按退避策略回队

-
- 0 ? styles.metricDanger : ''}`}> -
死信任务
- - -
-
- -
- 运行容量当前并发槽位使用情况}> -
- {activeQueue.utilization}%容量利用率 - -
- -
- - 调度策略服务端生效中的 Cron 配置}> -
-
运行任务{activeQueue.crontab.run}
-
检查任务{activeQueue.crontab.check}
-
回收过期{activeQueue.crontab.expire}
-
-
- - 投递生命周期该队列在当前进程中的累计状态变化}> -
-
回调成功{metric(activeQueue.callbacks?.success)}
-
回调失败 0 ? styles.dangerText : undefined}>{metric(activeQueue.callbacks?.failure)}
-
任务认领{metric(activeQueue.claims?.claimed)}
-
租约恢复{metric(activeQueue.claims?.recovered)}
-
-
-
+ +
+ } value={activeQueue.waiting} suffix="个" tone={activeQueue.waiting > 0 ? 'warning' : 'default'} footer={<>最老等待:{formatAge(activeQueue.oldestWaitingAgeSeconds, activeQueue.waiting)}} /> + + + } value={activeQueue.running} suffix={`/ ${activeQueue.concurrency}`} progress={activeQueue.utilization} footer={<>当前可用槽位:{activeQueue.available}} /> + + + } value={metric(activeQueue.retrying)} suffix="个" tone={(activeQueue.retrying ?? 0) > 0 ? 'warning' : 'default'} footer="失败后按退避策略回队" /> + + + } value={metric(activeQueue.deadLetters)} suffix="个" tone={(activeQueue.deadLetters ?? 0) > 0 ? 'danger' : 'default'} footer={} /> + + + + + + 运行容量当前并发槽位使用情况}> +
+ {activeQueue.utilization}%容量利用率 + +
+ +
+ + + + 调度策略服务端生效中的 Cron 配置}> +
+
运行任务{activeQueue.crontab.run}
+
检查任务{activeQueue.crontab.check}
+
回收过期{activeQueue.crontab.expire}
+
+
+ + + + 投递生命周期该队列在当前进程中的累计状态变化}> + + + 0 ? styles.dangerStatistic : undefined} title="回调失败" value={metric(activeQueue.callbacks?.failure)} /> + + + + + + ) : ( @@ -922,7 +1054,6 @@ const Dashboard: NextPage = () => { const renderDeadLetters = () => ( <> entryId 仅用于代际校验,不进入指标标签或应用日志。} @@ -942,20 +1073,22 @@ const Dashboard: NextPage = () => { {activeQueue ? ( - 0 ? 'warning' : 'success'} - showIcon - message={`${activeQueue.namespace} · ${currentDeadLetters?.total ?? metric(activeQueue.deadLetters)} 条死信`} - description="重放会重新进入等待队列,并保留完整的幂等与代际保护。" - /> + +
+ {activeQueue.namespace} + 重放会重新进入等待队列,并保留完整的幂等与代际保护。 +
+ 0 ? 'warning' : 'success'}> + {currentDeadLetters?.total ?? metric(activeQueue.deadLetters)} 条死信 + +
}} />
{(currentDeadLetters?.items ?? []).map((item) => ( -
{item.taskId}{item.reason === 'lease_expired' ? '租约过期' : '回调失败'}
+
{item.taskId}{item.reason === 'lease_expired' ? '租约过期' : '回调失败'}
已重试 {item.retryCount} 次{formatTimestamp(item.failedAt)}
@@ -989,46 +1122,51 @@ const Dashboard: NextPage = () => { return ( <> 最近检查:{formatTimestamp(health.checkedAt)}} /> -
+ {probes.map((probe) => ( - - {probe.icon} - {probe.title}{probe.path} - - +
+ + {probe.icon} + {probe.title}{probe.path} + + + ))} - - -
- 探针契约适用于编排系统与负载均衡器}> - /health/live }, - { key: 'ready', label: 'Readiness', children: /health/ready }, - { key: 'cache', label: '缓存策略', children: 'readiness 禁止缓存' }, - { key: 'failure', label: '失败状态', children: '依赖异常时返回 HTTP 503' }, - ]} - /> - - - 指标契约低基数 Prometheus exposition}> -
-
服务端端点/metrics
-
访问方式Bearer 鉴权
-
浏览器代理不开放
-
Counter 语义进程重启清零
-
- -
-
+ + + + + 探针契约适用于编排系统与负载均衡器}> + /health/live }, + { key: 'ready', label: 'Readiness', children: /health/ready }, + { key: 'cache', label: '缓存策略', children: 'readiness 禁止缓存' }, + { key: 'failure', label: '失败状态', children: '依赖异常时返回 HTTP 503' }, + ]} + /> + + + + + 指标契约低基数 Prometheus exposition}> +
+
服务端端点/metrics
+
访问方式Bearer 鉴权
+
浏览器代理不开放
+
Counter 语义进程重启清零
+
+ +
+ + ); }; @@ -1042,7 +1180,7 @@ const Dashboard: NextPage = () => { 跳到主要内容 - +
@@ -1051,6 +1189,8 @@ const Dashboard: NextPage = () => { className={styles.railMenu} mode="inline" aria-label="运行模块" + classNames={{ itemContent: styles.railMenuContent }} + styles={RAIL_MENU_STYLES} items={menuItems} selectedKeys={[view]} onClick={({ key }) => setView(key as ViewKey)} @@ -1069,13 +1209,14 @@ const Dashboard: NextPage = () => {
- {catalog} + {catalog}
更新于 {formatTimestamp(overview?.generatedAt)} @@ -1083,7 +1224,6 @@ const Dashboard: NextPage = () => {
@@ -1111,16 +1251,12 @@ const Dashboard: NextPage = () => { )} -
- 数据源:/waitqueue/admin/overview · 10 秒刷新 - 仅呈现实时快照,不推断历史趋势 -
- setMobileCatalogOpen(false)} styles={{ body: { padding: 0 } }}>{catalog} + setMobileCatalogOpen(false)} styles={{ body: { padding: 0 } }}>{catalog} setQueueModalOpen(false)} footer={null} destroyOnHidden>
diff --git a/admin-dashboard/src/style/dashboard.module.css b/admin-dashboard/src/style/dashboard.module.css index 479a9fe..806d333 100644 --- a/admin-dashboard/src/style/dashboard.module.css +++ b/admin-dashboard/src/style/dashboard.module.css @@ -1,6 +1,5 @@ .shell { min-height: 100vh; - background: var(--ui-canvas) !important; } .skipLink { @@ -13,7 +12,7 @@ background: var(--ui-primary); border-radius: 8px; font-size: 13px; - font-weight: 650; + font-weight: 600; text-decoration: none; transform: translateY(-160%); transition: transform var(--ui-duration-fast) var(--ui-ease); @@ -23,16 +22,24 @@ transform: translateY(0); } +.srOnly { + position: absolute; + overflow: hidden; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + white-space: nowrap; + border: 0; + clip-path: inset(50%); +} + .moduleRail { - position: sticky !important; + position: sticky; z-index: 30; top: 0; height: 100vh; - flex: 0 0 var(--ui-rail-width) !important; - min-width: var(--ui-rail-width) !important; - max-width: var(--ui-rail-width) !important; - width: var(--ui-rail-width) !important; - background: var(--ui-navigation) !important; + height: 100dvh; border-right: 1px solid var(--ui-border); } @@ -67,45 +74,16 @@ .brandMark:hover { color: var(--ui-primary-hover); box-shadow: var(--ui-shadow-md); - transform: translateY(-1px); } .railMenu { - width: 100% !important; - flex: 1; - background: transparent !important; - border: 0 !important; -} - -.railMenu > :global(.ant-menu-item) { - display: flex; - width: 52px; - height: 56px; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 3px; - margin: 4px 2px !important; - padding: 5px 2px !important; - line-height: 1; -} - -.railMenu :global(.ant-menu-item .ant-menu-item-icon) { - display: inline-grid; - min-width: 19px; - margin: 0 !important; - place-items: center; + min-height: 0; } -.railMenu :global(.ant-menu-title-content) { - width: 100%; - margin: 0 !important; - overflow: visible; +.railMenuContent { font-size: 12px; font-weight: 600; line-height: 1.2; - text-align: center; - text-overflow: clip; } .railFooter { @@ -121,16 +99,12 @@ } .contextSidebar { - position: sticky !important; + position: sticky; z-index: 25; top: 0; height: 100vh; - flex: 0 0 var(--ui-sidebar-width) !important; - min-width: var(--ui-sidebar-width) !important; - max-width: var(--ui-sidebar-width) !important; - width: var(--ui-sidebar-width) !important; + height: 100dvh; overflow: hidden; - background: var(--ui-canvas-subtle) !important; border-right: 1px solid var(--ui-border); } @@ -140,7 +114,6 @@ min-height: 0; flex-direction: column; color: var(--ui-foreground); - background: var(--ui-canvas-subtle); } .catalogHeader { @@ -174,7 +147,7 @@ .catalogHeaderCopy strong { overflow: hidden; font-size: 14px; - font-weight: 680; + font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } @@ -195,11 +168,6 @@ padding: 16px 14px 10px; } -.catalogSearch :global(.ant-input-affix-wrapper) { - background: var(--ui-surface-raised); - border-color: var(--ui-control-border); -} - .catalogSearch :global(.ant-input-prefix) { color: var(--ui-foreground-subtle); } @@ -211,50 +179,27 @@ padding: 10px 18px 7px; color: var(--ui-foreground-muted); font-size: 12px; - font-weight: 650; + font-weight: 600; } .queueNav { - display: flex; min-height: 0; flex: 1; - flex-direction: column; - gap: 3px; overflow: auto; padding: 0 10px 16px; scrollbar-width: thin; } -.queueNav > button { - display: grid; - min-height: 58px; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; - gap: 10px; - padding: 8px 9px; - color: var(--ui-foreground-muted); - text-align: left; - background: transparent; - border: 1px solid transparent; - border-radius: 10px; - cursor: pointer; - transition: - color var(--ui-duration-fast) var(--ui-ease), - background var(--ui-duration-fast) var(--ui-ease), - border-color var(--ui-duration-fast) var(--ui-ease), - box-shadow var(--ui-duration-fast) var(--ui-ease); -} - -.queueNav > button:hover { - color: var(--ui-foreground); - background: color-mix(in srgb, var(--ui-surface) 72%, transparent); +.queueMenu { + min-width: 0; } -.queueNav > button.queueNavActive { - color: var(--ui-foreground); - background: var(--ui-surface-raised); - border-color: var(--ui-control-border); - box-shadow: inset 2px 0 var(--ui-primary), var(--ui-shadow-sm); +.queueMenuLabel { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 8px; } .queueAvatar { @@ -296,7 +241,7 @@ overflow: hidden; color: inherit; font-size: 13px; - font-weight: 650; + font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } @@ -329,10 +274,7 @@ } .catalogEmpty { - margin: 28px 10px; - color: var(--ui-foreground-muted); - font-size: 12px; - text-align: center; + margin-block: 28px; } .catalogFooter { @@ -343,19 +285,17 @@ border-top: 1px solid var(--ui-border); } -.catalogFooter > span { +.catalogFooter > :global(.ant-typography) { display: flex; align-items: center; justify-content: center; gap: 5px; - color: var(--ui-foreground-muted); font-size: 12px; } .mainLayout { min-width: 0; min-height: 100vh; - background: var(--ui-canvas) !important; } .topbar { @@ -367,11 +307,7 @@ align-items: center; justify-content: space-between; gap: 20px; - padding: 0 24px !important; - line-height: normal !important; - background: color-mix(in srgb, var(--ui-canvas) 90%, transparent) !important; border-bottom: 1px solid var(--ui-border); - backdrop-filter: blur(14px); } .topbarContext, @@ -379,14 +315,19 @@ display: flex; min-width: 0; align-items: center; + line-height: normal; } .topbarContext { gap: 8px; } -.topbarContext :global(.ant-breadcrumb) { +.topbarDescription { + overflow: hidden; + max-width: 420px; font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; } .topbarActions { @@ -398,7 +339,6 @@ display: inline-flex; height: 26px; align-items: center; - margin: 0; padding-inline: 9px; border-radius: 999px; font-size: 12px; @@ -412,7 +352,7 @@ } .mobileMenu { - display: none !important; + display: none; } .spin { @@ -421,7 +361,6 @@ .content { min-width: 0; - background: var(--ui-canvas) !important; } .page { @@ -429,7 +368,7 @@ max-width: 1408px; min-height: calc(100vh - var(--ui-topbar-height)); margin: 0 auto; - padding: 40px 32px 24px; + padding: 32px 32px 24px; } .alert { @@ -442,21 +381,15 @@ .detailCard, .queueHealthCard, .recoveryCard { - border-color: var(--ui-border) !important; box-shadow: var(--ui-shadow-sm); } -.initialSkeleton { - padding: 18px; - background: var(--ui-surface) !important; -} - .pageHeader { display: flex; - align-items: flex-end; + align-items: center; justify-content: space-between; gap: 32px; - margin-bottom: 28px; + margin-bottom: 22px; animation: ui-fade-in var(--ui-duration-normal) var(--ui-ease); } @@ -464,28 +397,17 @@ min-width: 0; } -.pageEyebrow { - margin-bottom: 8px; - color: var(--ui-primary); - font-size: 12px; - font-weight: 700; - letter-spacing: 0.055em; - text-transform: uppercase; -} - -.pageHeader h1 { +.pageTitle { margin: 0; - color: var(--ui-foreground); font-size: clamp(24px, 2.4vw, 26px); - font-weight: 680; + font-weight: 600; letter-spacing: -0.025em; line-height: 1.2; } -.pageHeaderMain > p { +.pageDescription { max-width: 700px; margin: 8px 0 0; - color: var(--ui-foreground-muted); font-size: 14px; line-height: 1.6; } @@ -503,7 +425,6 @@ .pageMeta :global(.ant-tag) { display: inline-flex; align-items: center; - margin: 0; border-radius: 999px; } @@ -519,65 +440,38 @@ } .metricGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; margin-bottom: 16px; animation: ui-fade-in var(--ui-duration-normal) var(--ui-ease); } +.metricGrid > :global(.ant-col) { + display: flex; +} + .metricCard { - position: relative; + width: 100%; + height: 100%; min-width: 0; - overflow: hidden; - background: var(--ui-surface-raised) !important; - border-color: var(--ui-border) !important; - border-radius: 14px !important; + border-top: 2px solid transparent; box-shadow: var(--ui-shadow-sm); - transition: - transform var(--ui-duration-normal) var(--ui-ease), - box-shadow var(--ui-duration-normal) var(--ui-ease); } -.metricCard::before { - position: absolute; - z-index: 1; - top: 0; - right: 0; - left: 0; - height: 3px; - background: var(--ui-primary); - content: ''; -} - -.metricCard:hover { - box-shadow: var(--ui-shadow-md); - transform: translateY(-2px); -} - -.metricWarning::before { - background: var(--ui-warning); +.metricWarning { + border-top-color: var(--ui-warning); } -.metricDanger::before { - background: var(--ui-danger); +.metricDanger { + border-top-color: var(--ui-danger); } .metricCard :global(.ant-card-body) { display: flex; - min-height: 178px; + min-height: 140px; flex-direction: column; - padding: 20px; } -.metricCardHeader { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 20px; - color: var(--ui-foreground-muted); - font-size: 13px; - font-weight: 650; +.metricCard :global(.ant-statistic-title) { + margin-bottom: 12px; } .metricIcon { @@ -600,13 +494,8 @@ background: var(--ui-danger-soft); } -.metricCard :global(.ant-statistic) { - margin-top: auto; -} - .metricCard :global(.ant-statistic-content) { - color: var(--ui-foreground); - font-weight: 700; + font-weight: 600; letter-spacing: -0.025em; } @@ -617,7 +506,7 @@ font-weight: 500; } -.metricCard > :global(.ant-card-body) > p { +.metricFooter { min-height: 20px; margin: 8px 0 0; color: var(--ui-foreground-muted); @@ -629,36 +518,26 @@ } .metricCard :global(.ant-btn-link) { - align-self: flex-start; height: auto; - margin-top: 7px; padding: 0; font-size: 12px; } .overviewGrid { - display: grid; - grid-template-columns: minmax(0, 2.2fr) minmax(280px, 0.8fr); - gap: 16px; - align-items: stretch; animation: ui-fade-in var(--ui-duration-normal) var(--ui-ease); } +.overviewGrid > :global(.ant-col) { + display: flex; +} + .queueHealthCard, .recoveryCard, .surfaceCard, .detailCard, .emptySurface { - background: var(--ui-surface) !important; - border-radius: 14px !important; -} - -.queueHealthCard :global(.ant-card-head), -.recoveryCard :global(.ant-card-head), -.detailCard :global(.ant-card-head) { - min-height: 66px; - padding-inline: 20px; - border-bottom-color: var(--ui-border); + width: 100%; + height: 100%; } .queueHealthCard :global(.ant-card-body) { @@ -684,7 +563,7 @@ .cardHeading strong { color: var(--ui-foreground); font-size: 14px; - font-weight: 680; + font-weight: 600; } .cardHeading small { @@ -694,28 +573,6 @@ font-weight: 400; } -.queueHealthCard :global(.ant-tag), -.queueHealthCard :global(.ant-card-extra) { - margin: 0; -} - -.queueHealthCard :global(.ant-table) { - background: transparent; -} - -.queueHealthCard :global(.ant-table-container) { - border-start-start-radius: 0 !important; - border-start-end-radius: 0 !important; -} - -.queueHealthCard :global(.ant-btn-link) { - color: var(--ui-primary); -} - -.queueHealthCard :global(.ant-btn-link:hover) { - color: var(--ui-primary-hover); -} - .queueIdentity { display: flex; min-width: 0; @@ -723,24 +580,16 @@ gap: 3px; } -.queueIdentity button { +.queueIdentity :global(.ant-btn-link) { overflow: hidden; width: fit-content; + height: auto; max-width: 100%; padding: 0; - color: var(--ui-foreground); - text-align: left; text-overflow: ellipsis; white-space: nowrap; - background: transparent; - border: 0; - cursor: pointer; font-size: 13px; - font-weight: 650; -} - -.queueIdentity button:hover { - color: var(--ui-primary); + font-weight: 600; } .queueIdentity span { @@ -752,7 +601,6 @@ } .queueIdentity code, -.footer code, .scheduleList code, .contractList code, .deadLetterHeader code, @@ -790,11 +638,11 @@ } .warningText { - color: var(--ui-warning) !important; + color: var(--ui-warning); } .dangerText { - color: color-mix(in srgb, var(--ui-danger) 92%, var(--ui-foreground)) !important; + color: color-mix(in srgb, var(--ui-danger) 92%, var(--ui-foreground)); } .mobileQueueList, @@ -804,34 +652,43 @@ .recoveryCard :global(.ant-card-body) { display: flex; - min-height: calc(100% - 66px); + min-height: 0; + flex: 1; flex-direction: column; padding: 12px 20px 18px; } -.counterList { +.recoveryCard { display: flex; flex-direction: column; } -.counterList > div { +.counterList { + margin-bottom: 16px; +} + +.counterList > :global(.ant-col) { display: flex; - min-height: 50px; - align-items: center; - justify-content: space-between; - gap: 16px; - border-bottom: 1px solid var(--ui-border); } -.counterList span { - color: var(--ui-foreground-muted); - font-size: 13px; +.counterList :global(.ant-statistic) { + width: 100%; + padding: 12px; + background: var(--ui-surface-inset); + border-radius: 10px; } -.counterList strong { - color: var(--ui-foreground); +.counterList :global(.ant-statistic-title) { + margin-bottom: 4px; + font-size: 12px; +} + +.counterList :global(.ant-statistic-content) { font-size: 20px; - font-variant-numeric: tabular-nums; +} + +.dangerStatistic :global(.ant-statistic-content) { + color: color-mix(in srgb, var(--ui-danger) 92%, var(--ui-foreground)); } .counterNote { @@ -854,14 +711,11 @@ } .queueDetailGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; animation: ui-fade-in var(--ui-duration-normal) var(--ui-ease); } -.detailCard :global(.ant-card-body) { - padding: 20px; +.queueDetailGrid > :global(.ant-col) { + display: flex; } .capacitySummary { @@ -920,44 +774,34 @@ white-space: nowrap; } -.lifecycleCard { - grid-column: 1 / -1; -} - -.callbackGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; +.callbackGrid > :global(.ant-col) { + display: flex; } -.callbackGrid > div { - display: flex; - min-height: 84px; - flex-direction: column; - justify-content: center; +.callbackGrid :global(.ant-statistic) { + width: 100%; padding: 14px 16px; background: var(--ui-surface-inset); border-radius: 10px; } -.callbackGrid span { - color: var(--ui-foreground-muted); +.callbackGrid :global(.ant-statistic-title) { + margin-bottom: 4px; font-size: 12px; } -.callbackGrid strong { - margin-top: 4px; - color: var(--ui-foreground); +.callbackGrid :global(.ant-statistic-content) { font-size: 22px; - font-variant-numeric: tabular-nums; } -.surfaceCard :global(.ant-card-body) { - padding: 20px; +.deadLetterSummary { + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--ui-border); } -.inlineAlert { - margin-bottom: 18px; +.deadLetterSummary :global(.ant-typography) { + margin: 4px 0 0; } .mobileDeadLetterList { @@ -966,8 +810,7 @@ } .deadLetterCard { - background: var(--ui-surface-raised) !important; - border-color: var(--ui-border) !important; + box-shadow: var(--ui-shadow-sm); } .deadLetterHeader { @@ -989,7 +832,6 @@ .deadLetterHeader :global(.ant-tag) { flex: none; - margin: 0; } .deadLetterMeta { @@ -1008,17 +850,17 @@ } .healthGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; margin-bottom: 16px; animation: ui-fade-in var(--ui-duration-normal) var(--ui-ease); } +.healthGrid > :global(.ant-col) { + display: flex; +} + .healthCard { - background: var(--ui-surface-raised) !important; - border-color: var(--ui-border) !important; - border-radius: 14px !important; + width: 100%; + height: 100%; box-shadow: var(--ui-shadow-sm); } @@ -1078,12 +920,13 @@ } .diagnosticsGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; animation: ui-fade-in var(--ui-duration-normal) var(--ui-ease); } +.diagnosticsGrid > :global(.ant-col) { + display: flex; +} + .contractList { display: flex; flex-direction: column; @@ -1111,26 +954,6 @@ font-weight: 600; } -.contractList :global(.ant-tag) { - margin: 0; -} - -.footer { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - margin-top: 28px; - padding: 14px 2px 0; - color: var(--ui-foreground-muted); - border-top: 1px solid var(--ui-border); - font-size: 12px; -} - -.footer code { - font-size: 11px; -} - .formGrid { display: grid; grid-template-columns: minmax(0, 1fr) 180px; @@ -1150,52 +973,13 @@ margin-top: 6px; } -@media (max-width: 1280px) { - .metricGrid, - .healthGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .overviewGrid { - grid-template-columns: 1fr; - } - - .recoveryCard :global(.ant-card-body) { - min-height: auto; - } - - .counterList { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 16px; - } - - .counterList > div { - min-height: 74px; - align-items: flex-start; - flex-direction: column; - justify-content: center; - gap: 3px; - padding: 12px; - background: var(--ui-surface-inset); - border: 0; - border-radius: 10px; - } -} - -@media (max-width: 1024px) { +@media (max-width: 991px) { .page { padding: 32px 28px 22px; } -} -@media (max-width: 900px) { - .contextSidebar { - flex-basis: 208px !important; - min-width: 208px !important; - max-width: 208px !important; - width: 208px !important; + .shell { + --ui-sidebar-width: 208px; } .catalogHeader, @@ -1208,20 +992,20 @@ } } -@media (max-width: 720px) { +@media (max-width: 767px) { .contextSidebar { display: none; } .mobileMenu { - display: inline-flex !important; + display: inline-flex; } - .moduleRail { - flex-basis: 60px !important; - min-width: 60px !important; - max-width: 60px !important; - width: 60px !important; + .shell { + --ui-rail-width: 60px; + --ui-rail-item-width: 50px; + --ui-rail-item-height: 50px; + --ui-topbar-inline: 14px; } .moduleRailInner { @@ -1233,28 +1017,17 @@ height: 44px; } - .railMenu > :global(.ant-menu-item) { - width: 50px; - height: 50px; - margin-inline: 1px !important; - } - - .railMenu :global(.ant-menu-title-content) { + .railMenuContent { position: absolute; width: 1px; height: 1px; padding: 0; - margin: -1px !important; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } - .topbar { - padding: 0 14px !important; - } - .topbarActions :global(.ant-btn), .mobileMenu { min-width: 44px; @@ -1270,7 +1043,7 @@ .pageActions :global(.ant-select-selector), .catalogSearch :global(.ant-input-affix-wrapper) { - min-height: 44px !important; + min-height: 44px; } .catalogDrawer :global(.ant-drawer-close), @@ -1287,16 +1060,10 @@ line-height: 42px; } - .updated, - .actionText { + .updated { display: none; } - .topbarActions :global(.ant-btn-primary) { - width: 38px; - padding-inline: 0; - } - .pageHeader { align-items: flex-start; flex-direction: column; @@ -1313,21 +1080,25 @@ flex: 1; } - .queueDetailGrid, - .diagnosticsGrid { - grid-template-columns: 1fr; +} + +@media (max-width: 575px) { + .topbar { + gap: 8px; } - .lifecycleCard { - grid-column: auto; + .topbarContext, + .topbarActions { + gap: 4px; } - .callbackGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .topbarActions :global(.ant-tag) { + width: 28px; + justify-content: center; + padding-inline: 0; + font-size: 0; } -} -@media (max-width: 680px) { .desktopTable { display: none; } @@ -1372,7 +1143,6 @@ .mobileQueueHeader :global(.ant-tag) { flex: none; - margin: 0; } .mobileQueueMetrics { @@ -1404,20 +1174,7 @@ padding: 14px; } - .footer { - align-items: flex-start; - flex-direction: column; - gap: 5px; - } -} - -@media (max-width: 560px) { - .topbarContext :global(.ant-breadcrumb li:first-child), - .topbarContext :global(.ant-breadcrumb li:nth-child(2)) { - display: none; - } - - .topbarActions :global(.ant-tag) { + .topbarDescription { display: none; } @@ -1425,21 +1182,15 @@ padding: 24px 14px 18px; } - .metricGrid, - .healthGrid, - .counterList { - grid-template-columns: 1fr; - } - .metricCard :global(.ant-card-body) { - min-height: 160px; + min-height: 136px; } .pageHeader { margin-bottom: 22px; } - .pageHeader h1 { + .pageTitle { font-size: 24px; } @@ -1447,10 +1198,6 @@ flex: 1; } - .callbackGrid { - grid-template-columns: 1fr; - } - .mobileQueueMetrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1474,7 +1221,7 @@ @media (forced-colors: active) { .brandMark, - .queueNav > button.queueNavActive, + .queueMenu :global(.ant-menu-item-selected), .metricCard, .healthCard, .mobileQueueCard { diff --git a/admin-dashboard/src/style/global.css b/admin-dashboard/src/style/global.css index c280113..3909710 100644 --- a/admin-dashboard/src/style/global.css +++ b/admin-dashboard/src/style/global.css @@ -28,15 +28,15 @@ --ui-ai-foreground: #fbfaff; --ui-ai-soft: #ebe7f6; --ui-info: #476e84; - --ui-info-soft: #e1edf2; + --ui-info-soft: #edf6fa; --ui-success: #537153; --ui-success-strong: #4c694c; - --ui-success-soft: #e0eadf; + --ui-success-soft: #edf3eb; --ui-warning: #8b672b; --ui-warning-strong: #805c20; - --ui-warning-soft: #f4e8cf; + --ui-warning-soft: #fff7e6; --ui-danger: #9a4e45; - --ui-danger-soft: #f4dfdb; + --ui-danger-soft: #fff1f0; --ui-overlay: rgb(40 41 37 / 42%); --ui-focus: #71866f; --ui-selection: #dce6d9; @@ -73,6 +73,7 @@ --ui-rail-width: 72px; --ui-sidebar-width: 256px; --ui-topbar-height: 60px; + --ui-topbar-inline: 24px; } :root[data-theme='dark'] { diff --git a/admin-dashboard/src/theme/control-room-theme.tsx b/admin-dashboard/src/theme/control-room-theme.tsx index 17b6911..fccc3b4 100644 --- a/admin-dashboard/src/theme/control-room-theme.tsx +++ b/admin-dashboard/src/theme/control-room-theme.tsx @@ -20,7 +20,6 @@ interface SemanticPalette { foregroundMuted: string; foregroundSubtle: string; border: string; - borderStrong: string; controlBorder: string; primary: string; primaryHover: string; @@ -44,10 +43,10 @@ const PALETTE: Record = { light: { canvas: '#f5f1e8', canvasSubtle: '#eee9df', navigation: '#e6e1d7', surface: '#fbf9f4', surfaceRaised: '#fffdf8', surfaceInset: '#f0ece3', foreground: '#282925', foregroundMuted: '#5f615b', - foregroundSubtle: '#6c6e67', border: '#d9d4ca', borderStrong: '#c6c1b7', controlBorder: '#929088', primary: '#5f735e', + foregroundSubtle: '#6c6e67', border: '#d9d4ca', controlBorder: '#929088', primary: '#5f735e', primaryHover: '#50644f', primaryForeground: '#fbfdf8', primarySoft: '#e2e9df', info: '#476e84', - infoSoft: '#e1edf2', success: '#537153', successSoft: '#e0eadf', warning: '#8b672b', - warningSoft: '#f4e8cf', danger: '#9a4e45', dangerSoft: '#f4dfdb', + infoSoft: '#edf6fa', success: '#537153', successSoft: '#edf3eb', warning: '#8b672b', + warningSoft: '#fff7e6', danger: '#9a4e45', dangerSoft: '#fff1f0', overlay: 'rgba(40, 41, 37, 0.42)', shadow: '0 10px 30px rgba(40, 41, 37, 0.09), 0 2px 8px rgba(40, 41, 37, 0.05)', shadowStrong: '0 24px 64px rgba(40, 41, 37, 0.16), 0 4px 12px rgba(40, 41, 37, 0.07)', @@ -55,7 +54,7 @@ const PALETTE: Record = { dark: { canvas: '#1e201d', canvasSubtle: '#242622', navigation: '#292b27', surface: '#272925', surfaceRaised: '#2e302c', surfaceInset: '#222420', foreground: '#efede7', foregroundMuted: '#b6b5ad', - foregroundSubtle: '#93948c', border: '#3f423c', borderStrong: '#555950', controlBorder: '#737b6e', primary: '#a1b69d', + foregroundSubtle: '#93948c', border: '#3f423c', controlBorder: '#737b6e', primary: '#a1b69d', primaryHover: '#b2c6ae', primaryForeground: '#1c251c', primarySoft: '#344035', info: '#8eb6ca', infoSoft: '#2c4049', success: '#9dba9a', successSoft: '#314132', warning: '#d4b069', warningSoft: '#493d28', danger: '#d99488', dangerSoft: '#4d302d', @@ -83,9 +82,13 @@ function themeConfig(mode: ColorMode): ThemeConfig { colorPrimary: colors.primary, colorPrimaryHover: colors.primaryHover, colorInfo: colors.info, + colorInfoBg: colors.infoSoft, colorSuccess: colors.success, + colorSuccessBg: colors.successSoft, colorWarning: colors.warning, + colorWarningBg: colors.warningSoft, colorError: colors.danger, + colorErrorBg: colors.dangerSoft, colorBgBase: colors.canvas, colorBgLayout: colors.canvas, colorBgContainer: colors.surface, @@ -95,8 +98,8 @@ function themeConfig(mode: ColorMode): ThemeConfig { colorFillTertiary: colors.canvasSubtle, colorText: colors.foreground, colorTextSecondary: colors.foregroundMuted, - colorTextTertiary: colors.foregroundSubtle, - colorTextQuaternary: colors.foregroundSubtle, + colorTextTertiary: supportingText, + colorTextQuaternary: supportingText, colorTextPlaceholder: supportingText, colorTextHeading: colors.foreground, colorTextLabel: colors.foregroundMuted, @@ -134,6 +137,8 @@ function themeConfig(mode: ColorMode): ThemeConfig { bodyBg: colors.canvas, headerBg: colors.canvas, headerColor: colors.foreground, + headerHeight: 60, + headerPadding: '0 var(--ui-topbar-inline, 24px)', siderBg: colors.navigation, lightSiderBg: colors.navigation, }, @@ -141,8 +146,9 @@ function themeConfig(mode: ColorMode): ThemeConfig { colorBgContainer: colors.surface, headerBg: 'transparent', extraColor: colors.foregroundMuted, - actionsBg: colors.surfaceInset, - paddingLG: 20, + bodyPadding: 20, + headerHeight: 64, + headerPadding: 20, }, Menu: { itemBg: 'transparent', @@ -156,17 +162,24 @@ function themeConfig(mode: ColorMode): ThemeConfig { itemHoverColor: colors.foreground, itemBorderRadius: 10, activeBarBorderWidth: 0, + darkItemBg: 'transparent', + darkSubMenuItemBg: 'transparent', + darkItemColor: colors.foregroundMuted, + darkItemSelectedBg: colors.surfaceRaised, + darkItemSelectedColor: colors.primary, + darkItemHoverBg: colors.surface, + darkItemHoverColor: colors.foreground, + darkGroupTitleColor: colors.foregroundSubtle, + darkItemDisabledColor: colors.foregroundSubtle, }, Table: { colorText: colors.foreground, headerBg: colors.surfaceInset, headerColor: colors.foregroundMuted, rowHoverBg: colors.surfaceRaised, - rowSelectedBg: colors.primarySoft, - rowSelectedHoverBg: colors.primarySoft, borderColor: colors.border, - cellPaddingBlock: 13, - cellPaddingInline: 14, + cellPaddingBlockSM: 10, + cellPaddingInlineSM: 12, }, Button: { primaryColor: colors.primaryForeground,