Skip to content

feat(plugin-dashboard): dataset-bound metric 卡接上已声明的 colorVariant 强调色 (#3359) - #3799

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3359-dataset-widget-colorvariant
Aug 8, 2026
Merged

feat(plugin-dashboard): dataset-bound metric 卡接上已声明的 colorVariant 强调色 (#3359)#3799
yinlianghui merged 1 commit into
mainfrom
claude/issue-3359-dataset-widget-colorvariant

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3359

objectstack#5010 维护者裁决 B 的 objectui 半边实现单,spec 零改动。

前提复核(实读 objectui origin/main@f9d70a72e / objectstack origin/main@ea1d9165d)

前提成立,且比正文描述多一层字节级证据:

  1. datasetDashboardWidgetSchema必填(objectstack packages/spec/src/ui/dashboard.zod.ts),因此所有合法 widget 经 DashboardRenderer.tsx 两处派发口(isSelfContained 分支的 dataset-bound 口、Card 分支的 dataset-bound 口)恒走 DatasetWidget;isSelfContained = widget.type === 'metric' && !datasetBound,dataset-bound metric 落在后者。
  2. grep -c colorVariant packages/plugin-dashboard/src/DatasetWidget.tsx = 0;邻近词反查 categoryColors = 4,证伪「扫描器/路径错」。
  3. 字节级实证:改动前在 origin/main 上取证,声明 colorVariant: 'success' 与完全不声明,渲染出的 DOM 逐字符相同:
PROBE_UNDECLARED → div.class="flex h-full w-full flex-col items-start justify-center gap-1 p-2"
span.class="text-2xl font-semibold tabular-nums" 510000
span.class="text-xs text-muted-foreground" revenue
PROBE_SUCCESS → (与上完全一致,零差异)

(为绕开 GitHub body 的 HTML 清洗,上面用 标签.class= 代替尖括号书写;完整字节串以字符串常量 BASELINE_UNDECLARED 落在测试文件里。)

  1. 授权面复核:16 处真实授权全部是 dataset-bound 的 type: 'metric' —— platform-objects/src/apps/dashboards/system_overview.dashboard.ts 7 处,examples/app-showcaseops-dashboard 4 处 + revenue-pulse 5 处。用到的 token 恰好是 7 个非 default 强调色的全集。(app-showcase 各 page 里还有若干 object-metric 组件块也写 colorVariant,那是 properties 里传给 ObjectMetricWidget 的另一条已生效路径,不在本单计数内。)

实现

DatasetWidget 的 metric 卡读 widget.colorVariant,映射到本包已有的强调色体系,不新造一套:

  • 落点选择:dataset-bound metric 既不画 icon chip、也不自带 Card 外框,结构上正是 MetricWidgetbare 布局 —— 所以强调色落在大数字的文字色上,复用 VARIANT_TEXT_CLASSES。同一个声明在 inline bare KPI 与 dataset-bound KPI 上从此读起来一致。
  • 一张表,不是两张:把 VARIANT_ICON_CLASSES / VARIANT_TEXT_CLASSESMetricWidget.tsx 提到新的 src/colorVariants.ts,两个布局共用(纯搬移,MetricWidget 渲染零变化;MetricColorVariant 在原处 re-export 保持源码兼容)。设计器那份色板的注释本来就自称 "mirrors the renderer's colorVariant tokens",复制第二份正是把 declared≠enforced 换成更难查的 declared≠declared。
  • metricAccentTextClass(value) 对三种输入统一返回 undefined(即不追加任何 class):未声明、枚举自带的 'default'(它就是「无强调色」的名字)、以及任何非 spec token。不做 ?? 兜底、不认别名 —— 非法 token 由 spec 的 Zod 枚举在授权/发布处拒绝,消费端再认一种拼法就是给 AI 生成的元数据造第二套事实契约(AGENTS.md #0.1);同时一个纯装饰键也不该让整个 widget 渲染失败,所以是「无强调色」而非报错。

词表三方对照(spec ↔ 设计器 ↔ 渲染器)

spec 枚举实读:objectstack packages/spec/src/ui/dashboard.zod.ts:28WidgetColorVariantSchema

spec 枚举(8)设计器 color-variant-field.tsx:19-27(canonical 8)渲染器 colorVariants.ts → 大数字文字色
defaultdefault / #9ca3af不追加 class(与未声明逐字节一致)
blueblue / #3b82f6text-blue-600 dark:text-blue-400
tealteal / #14b8a6text-teal-600 dark:text-teal-400
orangeorange / #f97316text-orange-600 dark:text-orange-400
purplepurple / #a855f7text-purple-600 dark:text-purple-400
successsuccess / #22c55etext-emerald-600 dark:text-emerald-400
warningwarning / #f59e0btext-amber-600 dark:text-amber-400
dangerdanger / #ef4444text-rose-600 dark:text-rose-400

三方完全对齐,零自造变体。两处机械钉住,不靠这张表的人工核对:

  • DatasetWidget.colorVariant.test.tsx@objectstack/spec/uiWidgetColorVariantSchema.options,断言渲染器两张表的 key 集合 === spec 枚举(并先断言枚举非空,避免空列表下的空转绿)。
  • 新增 packages/app-shell/src/views/metadata-admin/color-variant-field.test.tsx(纯测试,无源码改动)把设计器 canonical 行按 spec 顺序钉到同一枚举上。此前这条腿完全没有机械约束。

设计器那份色板在 canonical 8 之后还有 3 条 green/red/amber,文件自己注明是「让非 canonical token 也能显示一个合理色块」的显示用别名,且 ColorVariantPicker 只渲染 slice(0, 8)。它们不是渲染器词表:新测试同时钉住「额外条目必须不是 spec token,且必须复用某个 canonical 色值」,防止将来有人把别名悄悄升格成第 9 个可授权变体。

缺省不回归证据

改动前先在 origin/main@f9d70a72e 上取字节基线(上文 PROBE),再把它写成测试里的字符串常量 BASELINE_UNDECLARED(不是 snapshot 文件 —— 回归要在 review diff 里看得见,也不能靠删过期快照「修好」)。断言:

  • 未声明 colorVariantcontainer.innerHTML全等基线;
  • colorVariant: 'default' → 全等基线;
  • 非 spec token(chartreuse / green / red / amber / '' / #ff0000)→ 全等基线;
  • 非字符串(对象)→ 全等基线,不抛异常;
  • 7 个强调色 → innerHTML 恰好等于「基线把大数字 class 换成 基础 class + 该强调色」,即只有一处差异;
  • 带 compareTo 的趋势行同理只差那一处 —— 趋势的红绿是数据驱动的,不能被 widget 强调色带跑。

反向验证(方向事先声明)

本单两个方向同时存在,模板不能一概而论,所以事先写明预测再跑:

  • 「缺省不回归」半边:改动前绿、改动后也绿。它是防回归钉,不是功能证据;把 accent 读数删掉它必须仍然绿
  • 「各枚举值映射」半边:改动前红、改动后绿

实测(把 const accentClass = metricAccentTextClass(widget?.colorVariant) 临时改为常量 undefined 后重跑同一测试文件):

× tints the value with the blue accent
× tints the value with the teal accent
× tints the value with the orange accent
× tints the value with the purple accent
× tints the value with the success accent
× tints the value with the warning accent
× tints the value with the danger accent
× renders all eight enum values distinguishably
× keeps the comparison trend row untouched by the accent
Tests 9 failed | 14 passed (23)

9 红全部落在功能半边,14 绿是缺省半边 + 词表 parity —— 与事先声明的方向一致。随后已复原。

验证

# 依赖先构建,防 TS2307 假红
pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build → 全部 Done
pnpm vitest run packages/plugin-dashboard \
packages/app-shell/src/views/metadata-admin/color-variant-field.test.tsx --maxWorkers=2
→ Test Files 28 passed (28) / Tests 246 passed (246)
(其中新增 DatasetWidget.colorVariant.test.tsx 23 项、color-variant-field.test.tsx 5 项)
pnpm --filter @object-ui/plugin-dashboard type-check → tsc --noEmit 无输出(通过)
pnpm --filter @object-ui/app-shell type-check → tsc --noEmit && tsc -p tsconfig.typetests.json 无输出(通过)
eslint(4 个改动/新增文件) → 0 error
node scripts/check-changeset-no-major.mjs → No changeset declares a `major` bump
node scripts/check-changeset-fixed.mjs → All workspace packages in the fixed group
node scripts/check-changeset-presence.mjs → 1 changeset declared ✅
grep -naP 控制字节扫描(全部改动文件) → 零命中;file(1) 全部 UTF-8 text

重型步骤全部在 flock /tmp/os-heavy-verify.lock 下串行、--max-old-space-size=4096--maxWorkers=2

Changeset

.changeset/dataset-widget-colorvariant-3359.md,@object-ui/plugin-dashboard: patch。档位依据仓内同类先例:同一组件同族的 declared→enforced 改动 compareto-kind-convergence.md(范围更大)也是 patch;minor 在本仓的现存两例都是公共 API 破坏性改名。app-shell 只加了测试、无用户可见变化,故不进 changeset 条目。README 补了 colorVariant 一节(AGENTS.md #2)。

范围外(未在本 PR 触碰)

  • 正文第 3 条 framework 侧 liveness/dashboard.json 该行翻 live + 解除 objectstack PR #5255 钉的「恰好只警告 colorVariant」正向对照 —— 按分诊席点名,由验收本 PR 的执行座位在 objectstack 立单承办(带 Blocked-by: 本 PR)。本 PR 不改 objectstack 仓。
  • inline 旧路径的 componentSchema 色彩逻辑未被搬用(正文第 3 条禁令)。

Generated by Claude Code

…d colorVariant (#3359)
`DashboardWidgetSchema.widgets[].colorVariant` was spec-declared, offered by
every authoring surface, and authored 16 times in shipped metadata — yet
`dataset` is REQUIRED on the widget schema, so every legal widget reaches
`DatasetWidget`, which read the key nowhere. Declared but never enforced
(objectstack#5010 ruling B).
The metric card now reads it and maps it onto the accent system this package
already has: the big number is tinted the way `MetricWidget`'s chrome-less
`bare` layout tints it, since a dataset-bound metric renders no icon chip and
no card of its own. Both class tables moved into one shared `colorVariants.ts`
so the two layouts cannot drift into disagreeing palettes.
Vocabulary is the spec's `WidgetColorVariantSchema` enum, read from the spec in
the tests rather than restated. Off-spec tokens (including the designer swatch
picker's display-only `green`/`red`/`amber` aliases) get no accent and no
aliasing — invalid metadata is rejected where it is authored, not laundered
here (AGENTS.md #0.1).
Undeclared, and the enum's own `default`, render byte-for-byte as before —
pinned against the origin/main@f9d70a72e capture.
@vercel

vercelBot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 8, 2026 4:29pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation plugin tests labels Aug 8, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-lZWT7_mZ.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)482.20KB106.20KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.85KB3.18KB
data-objectstack (index.js)139.51KB35.97KB
fields (index.js)230.82KB56.70KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)117.21KB30.27KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.64KB57.40KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.71KB49.68KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。

git 实物核验(d60023cce):净 diff 7 文件;metric 卡读 widget.colorVariant 复用既有 VARIANT_TEXT_CLASSES(两张表提为 colorVariants.ts 共用,MetricWidget 纯搬移零渲染变化);词表三方对照(spec 8 枚举 ↔ 设计器 canonical ↔ 渲染器映射)零自造变体,且对齐从人工核表升级为两处机械 parity 断言(spec 运行时推导;设计器那条此前无约束的腿顺手补上)—— 分诊席「不得 declared≠declared」的提示按更高标准落实。

验证质量:字节级基线(BASELINE_UNDECLARED 常量而非快照文件 —— 回归在 review diff 里可见,不能靠删快照「修好」)钉住缺省不回归;双向反向验证方向事先声明、实测 9 红全落映射半边与预测一致;非 spec token(含设计器 3 条显示用别名)不认不兜底不报错,并钉住「别名不得升格为第 9 个变体」。19 项 CI 全部完成 0 失败(PM 独立复核,dev 交单时在跑的 4 shard + Type Check 均已 success);changeset patch 依据同族先例,三门绿。

跨仓杂事已办:objectstack#6774 已立(liveness/dashboard.json 翻 live + 解除 PR #5255 正向对照,Blocked-by: objectui#3799)。环境提醒入方法库:scratchpad 跨 agent 不隔离(本单 pr-body.md 被并行 dev 覆写,第二例)—— 后续派发词将要求中间文件用带 issue 号的唯一文件名。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 16:36
@yinlianghui
yinlianghui added this pull request to the merge queueAug 8, 2026
Merged via the queue into main with commit c4c0ac8Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3359-dataset-widget-colorvariant branch August 8, 2026 16:37
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

2 participants

@yinlianghui@claude