diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 6cddb4f51..e5f21cdc6 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -66,17 +66,17 @@ registry。 | 指标 | 当前基线 | | --- | ---: | -| 扫描的生产前端文件数 | 1535 | +| 扫描的生产前端文件数 | 1536 | | 忽略的测试文件数 | 221 | | 包含颜色字面量的文件数 | 26 | -| 颜色字面量出现次数 | 1718 | +| 颜色字面量出现次数 | 1717 | | 唯一颜色字面量数量 | 913 | | 组件或非 token 文件中的颜色出现次数 | 0 | | 组件或非 token 唯一颜色数量 | 0 | | App UI 颜色出现次数 | 0 | | App UI 唯一颜色数量 | 0 | -| `var(--token, fallback)` 出现次数 | 25 | -| fallback 唯一 token 数 | 7 | +| `var(--token, fallback)` 出现次数 | 0 | +| fallback 唯一 token 数 | 0 | | token-equivalent app literal 出现次数 | 0 | | token-equivalent app literal 唯一颜色数量 | 0 | | 普通组件肉眼不可区分 near color pair | 0 | @@ -108,7 +108,13 @@ registry。 | compatibility alias family 直接使用次数 | 0 | generated widget frame 同时暴露 canonical 与 legacy family,内部 shell 读取 canonical family | | stale compatibility alias family contracts | 0 | 防止动态 family 或 canonical family 失配 | | missing compatibility alias family canonicals | 0 | 防止新增 `--radius-x` / `--spacing-x` 但缺少对应 canonical key | -| fallback token contracts | 7 | 每个 `var(--token, fallback)` 边界 fallback 都有 owner、reason 和 boundary | +| generated widget payload key | 326 | widget iframe 对外主题变量 allowlist,作为外部边界单独预算,不计入内部 alias 读取 | +| generated widget payload compatibility alias | 64 | payload 仍暴露已登记 legacy alias,防止已生成或第三方内容读取失败 | +| generated widget payload compatibility family key | 17 | payload 仍暴露 `--radius-*`、`--spacing-*` 具体 key,同时暴露 canonical family | +| generated widget payload undefined key | 0 | 防止 payload 引入没有静态、运行时或动态 family 定义的游离 key | +| generated widget payload missing compatibility canonical | 0 | 防止 payload 暴露 legacy key 但 canonical 目标不存在 | +| generated widget payload unexported compatibility canonical | 0 | 防止 payload 暴露 legacy key 但遗漏对应 canonical key | +| fallback token contracts | 0 | 组件 fallback 已通过根 token 或组件根默认值收敛;新增 fallback 会重新要求 owner、reason 和 boundary | | uncontracted fallback tokens | 0 | 防止新增未解释的 fallback key | | stale fallback token contracts | 0 | 防止已删除 fallback 继续留在 registry 中 | | color domain contracts | 13 | 每个专用域都有 owner、reason 和 merge policy | @@ -119,7 +125,7 @@ registry。 | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | | Theme presets | 1033 | 611 | 主题个性与 palette 映射,不作为普通 app literal 直接合并 | -| Token contracts | 268 | 159 | `tokens.scss` 等静态契约根 | +| Token contracts | 267 | 159 | `tokens.scss` 等静态契约根 | | Editor | 56 | 53 | Monaco/editor 专用域,不能直接泛化到 app token;组件装饰色已迁出 raw literal | | Mermaid | 139 | 95 | Mermaid 专用渲染域 | | Theme runtime | 54 | 45 | `ThemeService.ts` 运行时注入 | @@ -143,41 +149,32 @@ registry。 | `src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts` | 22 | isolated surface 和截图兜底值;集中 owner | | `src/web-ui/src/shared/theme/syntaxHighlightAccents.ts` | 18 | Prism syntax palette;不泛化到 app token | -当前 fallback token 都已进入 fallback contract registry,但仍需要逐项决策是否保留边界 fallback: +组件级 `var(--token, fallback)` 已收敛到 0;原先的 7 个 fallback token 不再需要 +fallback contract registry 保留。 -| fallback token | 次数 | -| --- | ---: | -| `--surface-stagger-index` | 12 | -| `--mission-control-group-color` | 6 | -| `--char-index` | 3 | -| `--gallery-grid-min` | 1 | -| `--gallery-skeleton-height` | 1 | -| `--primary-color` | 1 | -| `--scene-viewport-border-width` | 1 | - -fallback 决策表: +fallback 收敛决策表: -| fallback token | 决策 | 依据 | 后续动作 | +| 原 fallback token | 决策 | 依据 | 结果 | | --- | --- | --- | --- | -| `--surface-stagger-index` | 保留 | 运行时 inline 动画序号,`0` 是安全首帧/无动画默认值 | 不迁移为颜色 token;保持 contract | -| `--mission-control-group-color` | 保留 | 分组身份色由数据或 inline style 驱动,静态删除会丢失未设置分组色时的 accent 兜底 | 后续 content-canvas token 抽取时复核是否改为组件根默认值 | -| `--char-index` | 保留 | StreamText 每字符动画偏移,`0` fallback 是无序号渲染的安全默认值 | 不迁移为颜色 token;保持 contract | -| `--gallery-grid-min` | 保留 | runtime layout sizing 输入,不属于颜色债务;`320px` 保持 responsive grid 下限 | 保持 contract,后续只在 layout token 方案中处理 | -| `--gallery-skeleton-height` | 保留 | runtime skeleton sizing 输入,不属于颜色债务;`140px` 保持占位高度稳定 | 保持 contract,后续只在 layout token 方案中处理 | -| `--primary-color` | 延后 | Markdown 嵌入内容可覆盖 primary accent,边界语义不同于全局 app primary | Markdown token 抽取时决定是否转为 `--markdown-primary-color` contract | -| `--scene-viewport-border-width` | 保留 | viewport border width 是 runtime layout override,`1px` fallback 保持默认边界可见 | 保持 contract,后续只在 scene layout token 方案中处理 | +| `--surface-stagger-index` | 上移默认值 | `tokens.scss` 已提供 `0` 默认值,TS inline style 仍可覆盖动画序号 | 移除 12 处 selector fallback | +| `--mission-control-group-color` | 上移默认值并保留组别差异 | filter 仍由 inline style 驱动;thumbnail badge 的 primary/secondary/tertiary 默认值保持原 accent/success/warning 语义 | 移除 6 处背景 fallback,避免误把组别统一成同一颜色 | +| `--char-index` | 上移到组件根 | StreamText 根提供 `0`,每字符 inline style 仍可覆盖 | 移除 3 处 keyframe fallback | +| `--gallery-grid-min` | 上移到根 token 默认值 | `tokens.scss` 提供 `320px`,祖先变量和 props inline style 仍可覆盖 | 移除 grid sizing fallback | +| `--gallery-skeleton-height` | 上移到根 token 默认值 | `tokens.scss` 提供 `140px`,祖先变量和 props inline style 仍可覆盖 | 移除 skeleton height fallback | +| `--primary-color` | 改为 Markdown 组件 token | `--primary-color` 是 tool card 局部兼容入口,不提升为全局 app token;BaseToolCard 映射到 `--markdown-primary-color` | Markdown 内部不再读取旧 key fallback | +| `--scene-viewport-border-width` | 上移默认值 | 静态 token 提供 `1px`,ThemeService 继续按主题 layout 覆盖为 `1px` 或 `0` | 移除 viewport border fallback | 阶段状态: | 阶段 | 状态 | 当前判断 | | --- | --- | --- | | Phase 0:基线与工具 | 已完成主体 | 审计脚本可区分测试文件、fallback token、dynamic families 和 exception domains | -| Phase 1:canonical token 契约 | 已完成调用方迁移 | compatibility alias registry 已记录 64 个显式 alias 和 2 个 alias family;内部 `var()` 读取已清零,定义仍保留给旧主题、payload 和外部内容 | -| Phase 2:精确重复合并 | 已完成主体 | token-equivalent app literal 已清零;截图兜底、language identity 和 review/agent/insights 固定色已迁入显式 registry | -| Phase 3:legacy fallback 迁移 | 已强化 | fallback unique token 保持 7,且全部进入 fallback contract registry;新增未登记 fallback 会被审计报告和 baseline 拦截 | +| Phase 1:canonical token 契约 | 已完成调用方迁移 | compatibility alias registry 已记录 64 个显式 alias 和 2 个 alias family;内部 `var()` 读取已清零;widget payload 的 64 个显式 alias 和 17 个 family key 作为外部兼容面单独预算,并补齐 `--size-radius-2xl` / `--size-radius-full` canonical 导出 | +| Phase 2:精确重复合并 | 已完成主体 | token-equivalent app literal 已清零;截图兜底、language identity 和 review/agent/insights 固定色已迁入显式 registry;`tokens.scss` 中同域同语义的 deep-review consent 精确重复已改为 alias | +| Phase 3:legacy fallback 迁移 | 已完成主体 | 组件级 `var(--token, fallback)` 已清零,baseline 降到 0;新增 fallback 会被审计报告和 baseline 拦截 | | Phase 4:组件 token 抽取 | 已完成主体 | CodeEditor、StreamText、ChatInputPixelPet、ReferencesPanel、AgentCompanion、tool-card、editor 组件装饰色已抽为组件私有 RGB channel 或复用 contract token | | Phase 5:近似色合并 | 已完成主体 | 普通组件 near pair 已清零;极近似视觉色只在不相邻或不承担状态差异时合并,Monaco/terminal/Mermaid/syntax 专用 palette 不强行合并 | -| Phase 6:防回退约束 | 已强化 | baseline 已同步到 component/non-token=0、appUi=0、token-equivalent=0、nearPair=0、compatibility alias 读取=0,并保留 fallback、domain contract 防回退指标 | +| Phase 6:防回退约束 | 已强化 | baseline 已同步到 component/non-token=0、appUi=0、token-equivalent=0、nearPair=0、compatibility alias 读取=0、fallback=0,并保留 generated widget payload、domain contract 防回退指标 | Phase 5 决策记录: @@ -214,6 +211,12 @@ Phase 6 防回退约束: | `compatibilityAliases.staleRegisteredUnique` | 0 | 0 | 防止兼容 alias registry 保留没有定义或 canonical 目标缺失的 key | | `compatibilityAliases.staleRegisteredFamilyUnique` | 0 | 0 | 防止 `--radius-*`、`--spacing-*` 这类动态 family 与 canonical family 失配 | | `compatibilityAliases.missingCanonicalUnique` | 0 | 0 | 防止 family alias 具体 key 缺失对应 canonical key | +| `generatedWidgetPayload.varUnique` | 326 | 326 | 控制 widget 对外主题 payload allowlist 不继续膨胀 | +| `generatedWidgetPayload.compatibilityAliasUnique` | 64 | 64 | 控制 payload 中显式 legacy alias 数量,后续只能降低或经复审调整 | +| `generatedWidgetPayload.compatibilityAliasFamilyUnique` | 17 | 17 | 控制 payload 中 legacy size family 具体 key 数量 | +| `generatedWidgetPayload.undefinedUnique` | 0 | 0 | 防止 payload 导出未定义主题 key | +| `generatedWidgetPayload.missingCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 缺失 canonical 目标 | +| `generatedWidgetPayload.unexportedCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 有 canonical 定义但未导出到 iframe | | `fallbackContracts.uncontractedUnique` | 0 | 0 | 防止新增未说明边界的 `var(--token, fallback)` | | `fallbackContracts.staleRegisteredUnique` | 0 | 0 | 防止已删除 fallback 继续留在 registry 中 | | `colorDomainContracts.activeUncontractedUnique` | 0 | 0 | 防止新增专用颜色域但没有 owner 和 merge policy | @@ -234,7 +237,7 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 | tool-cards-review | tool card、review panel、expanded/collapsed/status | danger alias 保留 destructive 语义,不能和 error 无证据合并 | | code-editor-diff | Monaco、diff、selection、added/deleted/conflict | editor/diff 色表达相邻状态,不能按数值相似直接合并 | | terminal | ANSI normal/bright、selection、error | ANSI 语义独立于 app semantic color | -| markdown-mermaid | Markdown、Prism、Mermaid、diagram/error | `--primary-color` 是 embedded override;Mermaid 角色不等于 app status | +| markdown-mermaid | Markdown、Prism、Mermaid、diagram/error | Markdown accent 通过 `--markdown-primary-color` 表达;tool-card 的旧 `--primary-color` 只作为局部兼容输入映射,Mermaid 角色不等于 app status | | generated-widget | iframe fallback、host payload、loading/error | widget payload 兼容是保留多组旧 alias 的主要原因 | | theme-settings | theme switcher、system/custom theme preview | custom theme preview 可能比普通组件更早暴露 runtime alias 缺失 | | mobile-web-shell | mobile-web、mobile/narrow、loading/error/navigation | mobile web 是独立构建目标,不能只依赖 desktop WebView 验证 | @@ -419,19 +422,19 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 | 历史或漂移 token | 建议 canonical 目标 | 说明 | | --- | --- | --- | -| `--accent-primary` | `--color-accent-500` 或 `--color-primary` | 先明确 accent 与 primary 是否是两个角色。 | +| `--accent-primary` | `--color-accent-500` | `--color-primary` 也是同一 accent midpoint 的历史兼容名,新代码不应再以 primary 作为 canonical。 | | `--text-primary` | `--color-text-primary` | 仅兼容别名。 | | `--text-secondary` | `--color-text-secondary` | 仅兼容别名。 | | `--text-muted` | `--color-text-muted` | 仅兼容别名。 | | `--bg-primary` | `--color-bg-primary` | 仅兼容别名。 | | `--bg-secondary` | `--color-bg-secondary` | 仅兼容别名。 | | `--bg-tertiary` | `--color-bg-tertiary` | 仅兼容别名。 | -| `--border-primary` | `--border-base` | 需要确认调用点是否期望更强边界。 | -| `--color-border-subtle` | `--border-subtle` | 建议统一 border 命名族。 | -| `--color-danger` | `--color-error` | 仅当 destructive 与 validation error 不需要区分时合并。 | -| `--color-bg-hover` | `--element-bg-hover` | 需要确认调用点是 element、card 还是 panel hover。 | -| `--radius-*` | `--size-radius-*` | 对齐静态 CSS export 与运行时/widget 命名。 | -| `--spacing-*` | `--size-gap-*` | 对齐静态 CSS export 与运行时/widget 命名。 | +| `--border-primary` | `--border-base` | 当前 primary border 不表示更强层级,仅保留 legacy spelling。 | +| `--color-border-subtle` | `--border-subtle` | 统一到 border 命名族。 | +| `--color-danger` | `--color-error` | 当前共享 error palette,但保留 destructive action 语义;删除前需迁入 error 或 action token。 | +| `--color-bg-hover` | `--element-bg-hover` | 当前泛化 hover 已收敛到 element interaction layer。 | +| `--radius-*` | `--size-radius-*` | canonical family 为 `--size-radius-*`,旧 family 只作 legacy/widget payload 兼容。 | +| `--spacing-*` | `--size-gap-*` | canonical family 为 `--size-gap-*`,旧 family 只作 legacy/widget payload 兼容。 | ## 近似色合并规则 @@ -663,7 +666,7 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 - 对组件中新 app raw color 的 lint 或 audit 检查。 - 已知 exception file、namespace contract 与 owner。 -- compatibility alias、fallback token、color domain 的机器可校验 owner/reason contract。 +- compatibility alias、color domain 的机器可校验 owner/reason contract;fallback registry 维持为空并由 baseline 防回退。 - 覆盖 app-shell、Flow Chat、tool card/review、editor/diff、terminal、Mermaid/Markdown、 generated widget、theme settings 和 mobile web 的视觉证据契约。 - CI 在迁移期只阻止新增问题,不因历史 baseline 直接失败。 @@ -673,7 +676,7 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 - 新增组件级 raw color 必须有明确原因。 - 历史迁移可以按目录增量推进。 - exception 可见、可审查。 -- 兼容 alias 和边界 fallback 可见、可审查,且 stale contract 为 0。 +- 兼容 alias 可见、可审查;组件 fallback 保持 0,且 stale contract 为 0。 - CI 至少运行 `theme:color-audit:test`、`theme:color-audit` 和 `theme:visual-contract`。 @@ -828,7 +831,7 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 - 变更是否同时影响 light 和 dark theme。 - 变更是否影响 generated widget、code editor、terminal、Mermaid 或第三方内容。 - 删除 fallback 前,兼容 alias 是否已经存在。 -- 新增或保留的 compatibility alias、fallback token、color domain 是否进入对应 contract。 +- 新增或保留的 compatibility alias、color domain 是否进入对应 contract;新增 fallback 是否确有边界理由。 - 变更影响的 surface 是否已对照 `theme-visual-governance-contract.json` 确认覆盖形态。 - 高风险 surface 是否有截图或 focused visual check。 - PR 描述是否说明了任何用户可见视觉变化。 @@ -837,6 +840,8 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 建议按证据和 surface 拆分,避免一次性大迁移: +当前 PR 已覆盖前 5 项,并把 widget payload 外部兼容面纳入审计预算: + 1. 审计工具和 baseline report。 2. canonical token map 与 compatibility alias。 3. 静态和运行时 token 对齐。 @@ -858,21 +863,24 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 - 明确保留的近似色列表。 - 验证命令和结果。 -## 待决问题 +## 已审定兼容策略 -以下问题仍是产品语义决策,不再是未登记游离 key。当前已进入 +以下 key 不再视为未登记游离 key。当前已进入 `TOKEN_COMPATIBILITY_ALIAS_CONTRACTS` 或 `TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS`, -删除前必须先完成调用点迁移、widget payload 兼容检查和视觉复核。 - -- `--color-text-tertiary` 应转正为一等 semantic token,还是迁移到 - `--color-text-muted`。 -- `--color-primary` 和 `--color-accent-500` 是否是两个角色,还是应统一为 - 一个 accent contract。 -- `--color-danger` 是否需要和 `--color-error` 区分,以表达 destructive action。 -- 尺寸 token 长期应统一为 `--size-radius-*` / `--size-gap-*`,还是继续暴露 - `--radius-*` / `--spacing-*` 兼容名。 -- 迁移期 CI 应如何严格:只阻止新增 raw app color,还是按目录迁移完成后 - 对该目录启用失败约束。 +内部调用方必须使用 canonical token;删除旧 key 前必须先完成 widget payload 兼容检查、 +外部内容影响评估和视觉复核。 + +- `--color-text-tertiary` 当前不是一等 text ramp,兼容映射到 + `--color-text-muted`;只有设计系统确认需要独立第三层文本强度时才转正。 +- `--color-primary` 当前是 `--color-accent-500` 的历史兼容名;新代码应使用 + accent scale 或组件 action token,只有 primary action 与 accent 明确分化时才重新建模。 +- `--color-danger` 当前映射到 `--color-error`,但保留 destructive action 语义;只有破坏性动作 + 明确选择 error token 或迁入专用 action token 后才删除。 +- 尺寸 canonical family 是 `--size-radius-*` / `--size-gap-*`;`--radius-*` / + `--spacing-*` 只作为 legacy source 和 generated widget payload 兼容面保留。 +- 迁移期 CI 采用严格 baseline:普通 app raw color、内部 compatibility alias 读取、fallback、 + 未定义 CSS var、payload 未定义 key、payload 缺失 canonical 和 payload 未导出 canonical 均为 0; + payload 兼容 alias 数量只允许随迁移降低,不能无依据增长。 ## 完成标准 diff --git a/scripts/audit-theme-colors.mjs b/scripts/audit-theme-colors.mjs index a68d9833a..8876be3b6 100644 --- a/scripts/audit-theme-colors.mjs +++ b/scripts/audit-theme-colors.mjs @@ -34,6 +34,8 @@ const VAR_FALLBACK_PATTERN = /var\(\s*(--[a-zA-Z0-9_-]+)\s*,/g; const CSS_VAR_SET_PROPERTY_PATTERN = /\.setProperty\(\s*['"`](--[a-zA-Z0-9_-]+)/g; const CSS_VAR_INLINE_STYLE_PATTERN = /['"`](--[a-zA-Z0-9_-]+)['"`]\s*:/g; const CSS_VAR_DYNAMIC_SET_PATTERN = /\.setProperty\(\s*`(--[a-zA-Z0-9_-]*)\$\{/g; +const CSS_VAR_LITERAL_PATTERN = /['"`](--[a-zA-Z0-9_-]+)['"`]/g; +const GENERATED_WIDGET_THEME_PAYLOAD_PATH = 'tools/generative-widget/themePayload.ts'; const REPORT_ROW_LIMIT = 100; const COLOR_DOMAIN_CONTRACT_BY_KEY = new Map(COLOR_DOMAIN_CONTRACTS.map(contract => [contract.key, contract])); const FALLBACK_VAR_CONTRACT_BY_KEY = new Map(FALLBACK_VAR_CONTRACTS.map(contract => [contract.key, contract])); @@ -194,6 +196,10 @@ function isExceptionFile(relativePath) { return EXCEPTION_PATH_PARTS.some(part => relativePath.toLowerCase().includes(part.toLowerCase())); } +function isGeneratedWidgetThemePayloadFile(relativePath) { + return relativePath.endsWith(GENERATED_WIDGET_THEME_PAYLOAD_PATH); +} + function pathMatchesPart(relativePath, pathPart) { const normalizedPath = relativePath.toLowerCase(); const normalizedPart = pathPart.toLowerCase(); @@ -742,6 +748,8 @@ function audit(options) { const tokenAliasLiteralCounts = new Map(); const tokenAliasLiteralFiles = new Map(); const tokenAliasLiteralExamples = new Map(); + const generatedWidgetPayloadVarCounts = new Map(); + const generatedWidgetPayloadVarFiles = new Map(); let colorOccurrences = 0; let componentColorOccurrences = 0; @@ -828,6 +836,13 @@ function audit(options) { addToSetMap(dynamicDefinitionFiles, match[1], relativePath); } + if (isGeneratedWidgetThemePayloadFile(relativePath)) { + for (const match of collectMatches(content, CSS_VAR_LITERAL_PATTERN)) { + incrementMap(generatedWidgetPayloadVarCounts, match[1]); + addToSetMap(generatedWidgetPayloadVarFiles, match[1], relativePath); + } + } + for (const match of collectMatches(content, VAR_FALLBACK_PATTERN)) { fallbackOccurrences += 1; incrementMap(fallbackTokenCounts, match[1]); @@ -837,6 +852,9 @@ function audit(options) { const definedVars = new Set(varDefinitionCounts.keys()); const getDefinitionKinds = name => Array.from(varDefinitionKinds.get(name) ?? ['unknown']).sort(); + const getExplicitDefinitionKind = name => ( + definedVars.has(name) ? getDefinitionKinds(name).join('+') : null + ); const getDefinitionKind = name => { if (definedVars.has(name)) { return getDefinitionKinds(name).join('+'); @@ -996,6 +1014,62 @@ function audit(options) { files: entry.files, })) .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadVars = Array.from(generatedWidgetPayloadVarCounts.entries()) + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .map(([key, count]) => ({ + key, + count, + definitionKind: getExplicitDefinitionKind(key), + files: Array.from(generatedWidgetPayloadVarFiles.get(key) ?? []).sort().slice(0, 5), + })); + const generatedWidgetPayloadCompatibilityAliases = generatedWidgetPayloadVars + .map(entry => { + const contract = resolveCompatibilityAliasContract(entry.key); + if (!contract || contract.familyPrefix) { + return null; + } + return { + ...entry, + canonical: contract.canonical, + canonicalDefinitionKind: getExplicitDefinitionKind(contract.canonical), + }; + }) + .filter(Boolean) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)); + const generatedWidgetPayloadCompatibilityFamilies = generatedWidgetPayloadVars + .map(entry => { + const contract = resolveCompatibilityAliasContract(entry.key); + if (!contract?.familyPrefix) { + return null; + } + return { + ...entry, + canonical: contract.canonical, + familyPrefix: contract.familyPrefix, + canonicalPrefix: contract.canonicalPrefix, + canonicalDefinitionKind: getExplicitDefinitionKind(contract.canonical), + }; + }) + .filter(Boolean) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)); + const generatedWidgetPayloadUndefinedVars = generatedWidgetPayloadVars + .filter(entry => !entry.definitionKind) + .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadVarNames = new Set(generatedWidgetPayloadVars.map(entry => entry.key)); + const generatedWidgetPayloadMissingCompatibilityCanonicals = [ + ...generatedWidgetPayloadCompatibilityAliases, + ...generatedWidgetPayloadCompatibilityFamilies, + ] + .filter(entry => !entry.canonicalDefinitionKind) + .map(({ key, canonical, count, files }) => ({ key, canonical, count, files })) + .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadUnexportedCompatibilityCanonicals = [ + ...generatedWidgetPayloadCompatibilityAliases, + ...generatedWidgetPayloadCompatibilityFamilies, + ] + .filter(entry => entry.canonicalDefinitionKind && !generatedWidgetPayloadVarNames.has(entry.canonical)) + .map(({ key, canonical, count, files }) => ({ key, canonical, count, files })) + .sort((a, b) => a.key.localeCompare(b.key)); const staleCompatibilityAliasEntries = checksFullThemeSourceRoot ? TOKEN_COMPATIBILITY_ALIAS_CONTRACTS .map(contract => ({ @@ -1102,6 +1176,31 @@ function audit(options) { top: compatibilityAliasEntries.slice(0, options.top), families: compatibilityAliasFamilyEntries, }, + generatedWidgetPayload: { + varUnique: generatedWidgetPayloadVars.length, + occurrences: generatedWidgetPayloadVars.reduce((total, entry) => total + entry.count, 0), + undefinedUnique: generatedWidgetPayloadUndefinedVars.length, + compatibilityAliasUnique: generatedWidgetPayloadCompatibilityAliases.length, + compatibilityAliasOccurrences: generatedWidgetPayloadCompatibilityAliases.reduce( + (total, entry) => total + entry.count, + 0, + ), + compatibilityAliasFamilyUnique: generatedWidgetPayloadCompatibilityFamilies.length, + compatibilityAliasFamilyOccurrences: generatedWidgetPayloadCompatibilityFamilies.reduce( + (total, entry) => total + entry.count, + 0, + ), + missingCompatibilityCanonicalUnique: generatedWidgetPayloadMissingCompatibilityCanonicals.length, + unexportedCompatibilityCanonicalUnique: generatedWidgetPayloadUnexportedCompatibilityCanonicals.length, + topCompatibilityAliases: generatedWidgetPayloadCompatibilityAliases.slice(0, options.top), + topCompatibilityFamilies: generatedWidgetPayloadCompatibilityFamilies.slice(0, options.top), + undefinedVars: generatedWidgetPayloadUndefinedVars.slice(0, REPORT_ROW_LIMIT), + missingCompatibilityCanonicals: generatedWidgetPayloadMissingCompatibilityCanonicals.slice(0, REPORT_ROW_LIMIT), + unexportedCompatibilityCanonicals: generatedWidgetPayloadUnexportedCompatibilityCanonicals.slice( + 0, + REPORT_ROW_LIMIT, + ), + }, staleCompatibilityAliases: staleCompatibilityAliasEntries, staleCompatibilityAliasFamilies: staleCompatibilityAliasFamilyEntries, missingCompatibilityAliasCanonicals: missingCompatibilityAliasCanonicalEntries, @@ -1183,6 +1282,14 @@ function printText(report) { `staleFamilies=${report.compatibilityAliases.staleRegisteredFamilyUnique}, ` + `missingCanonicals=${report.compatibilityAliases.missingCanonicalUnique}` ); + console.log( + `Generated widget payload: vars=${report.generatedWidgetPayload.varUnique}, ` + + `undefined=${report.generatedWidgetPayload.undefinedUnique}, ` + + `compatAliases=${report.generatedWidgetPayload.compatibilityAliasUnique}, ` + + `compatAliasFamilies=${report.generatedWidgetPayload.compatibilityAliasFamilyUnique}, ` + + `missingCompatCanonicals=${report.generatedWidgetPayload.missingCompatibilityCanonicalUnique}, ` + + `unexportedCompatCanonicals=${report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique}` + ); console.log( `Fallback contracts: registered=${report.fallbackContracts.registeredUnique}, ` + `uncontracted=${report.fallbackContracts.uncontractedUnique}, ` + @@ -1280,6 +1387,57 @@ function printText(report) { } } + console.log('\nGenerated widget payload compatibility aliases:'); + if (report.generatedWidgetPayload.topCompatibilityAliases.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.topCompatibilityAliases.slice(0, 10)) { + console.log( + ` ${row.count.toString().padStart(5)} ${row.key} -> ${row.canonical} ` + + `canonicalDefined=${Boolean(row.canonicalDefinitionKind)}` + ); + } + } + + console.log('\nGenerated widget payload compatibility families:'); + if (report.generatedWidgetPayload.topCompatibilityFamilies.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.topCompatibilityFamilies.slice(0, 10)) { + console.log( + ` ${row.count.toString().padStart(5)} ${row.key} -> ${row.canonical} ` + + `canonicalDefined=${Boolean(row.canonicalDefinitionKind)}` + ); + } + } + + console.log('\nGenerated widget payload undefined vars:'); + console.log(printRows(report.generatedWidgetPayload.undefinedVars.slice(0, 10))); + + console.log('\nGenerated widget payload missing compatibility canonicals:'); + if (report.generatedWidgetPayload.missingCompatibilityCanonicals.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.missingCompatibilityCanonicals.slice(0, 10)) { + console.log( + ` ${row.key} -> ${row.canonical} ` + + `count=${row.count} files=${row.files.join(', ')}` + ); + } + } + + console.log('\nGenerated widget payload unexported compatibility canonicals:'); + if (report.generatedWidgetPayload.unexportedCompatibilityCanonicals.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.unexportedCompatibilityCanonicals.slice(0, 10)) { + console.log( + ` ${row.key} -> ${row.canonical} ` + + `count=${row.count} files=${row.files.join(', ')}` + ); + } + } + console.log('\nColor domain contract gaps:'); const colorDomainGapRows = [ ...report.missingColorDomainContracts.map(row => ({ ...row, count: 1 })), diff --git a/scripts/audit-theme-colors.test.mjs b/scripts/audit-theme-colors.test.mjs index 60aa6f98c..3ae1821ba 100644 --- a/scripts/audit-theme-colors.test.mjs +++ b/scripts/audit-theme-colors.test.mjs @@ -138,6 +138,9 @@ test('repository dynamic CSS var families match the registered contract', () => assert.equal(report.compatibilityAliases.staleRegisteredUnique, 0); assert.equal(report.compatibilityAliases.staleRegisteredFamilyUnique, 0); assert.equal(report.compatibilityAliases.missingCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); assert.equal(report.fallbackContracts.uncontractedUnique, 0); assert.equal(report.fallbackContracts.staleRegisteredUnique, 0); assert.equal(report.colorDomainContracts.missingRegisteredUnique, 0); @@ -256,6 +259,104 @@ test('theme color audit reports compatibility alias usage without treating it as assert.equal(report.colorScopes.appUi.occurrences, 0); }); +test('theme color audit budgets generated widget payload compatibility aliases separately', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --color-accent-500: #60a5fa;', + ' --color-primary: var(--color-accent-500);', + ' --size-radius-sm: 6px;', + ' --radius-sm: var(--size-radius-sm);', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + "export const payloadVars = ['--color-accent-500', '--color-primary', '--size-radius-sm', '--radius-sm'];", + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.compatibilityAliases.usedUnique, 0); + assert.equal(report.compatibilityAliases.familyUsedUnique, 0); + assert.equal(report.generatedWidgetPayload.varUnique, 4); + assert.equal(report.generatedWidgetPayload.compatibilityAliasUnique, 1); + assert.equal(report.generatedWidgetPayload.compatibilityAliasFamilyUnique, 1); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); + assert.deepEqual( + report.generatedWidgetPayload.topCompatibilityAliases.map(row => [row.key, row.canonical]), + [['--color-primary', '--color-accent-500']], + ); + assert.deepEqual( + report.generatedWidgetPayload.topCompatibilityFamilies.map(row => [row.key, row.canonical]), + [['--radius-sm', '--size-radius-sm']], + ); +}); + +test('theme color audit reports generated widget payload compatibility aliases without canonicals', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --radius-ghost: 10px;', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + "export const payloadVars = ['--radius-ghost'];", + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.compatibilityAliasFamilyUnique, 1); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 1); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); + assert.deepEqual( + report.generatedWidgetPayload.missingCompatibilityCanonicals.map(row => [row.key, row.canonical]), + [['--radius-ghost', '--size-radius-ghost']], + ); +}); + +test('theme color audit reports generated widget payload aliases whose canonicals are not exported', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --color-accent-500: #60a5fa;', + ' --color-primary: var(--color-accent-500);', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + "export const payloadVars = ['--color-primary'];", + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 1); + assert.deepEqual( + report.generatedWidgetPayload.unexportedCompatibilityCanonicals.map(row => [row.key, row.canonical]), + [['--color-primary', '--color-accent-500']], + ); +}); + test('theme color audit reports fallback tokens that lack a boundary contract', (t) => { const { dir, sourceRoot } = createFixture({ 'app/App.scss': [ diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index 18c483ddc..d76c26095 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -3,10 +3,10 @@ "description": "Baseline for Web UI theme color governance. Lower values when debt is removed; do not raise without a documented review reason.", "budgets": { "fallbackOccurrences": { - "max": 25 + "max": 0 }, "fallbackUniqueTokens": { - "max": 7 + "max": 0 }, "fallbackContracts.uncontractedUnique": { "max": 0 @@ -41,6 +41,33 @@ "compatibilityAliases.missingCanonicalUnique": { "max": 0 }, + "generatedWidgetPayload.varUnique": { + "max": 326 + }, + "generatedWidgetPayload.occurrences": { + "max": 326 + }, + "generatedWidgetPayload.undefinedUnique": { + "max": 0 + }, + "generatedWidgetPayload.compatibilityAliasUnique": { + "max": 64 + }, + "generatedWidgetPayload.compatibilityAliasOccurrences": { + "max": 64 + }, + "generatedWidgetPayload.compatibilityAliasFamilyUnique": { + "max": 17 + }, + "generatedWidgetPayload.compatibilityAliasFamilyOccurrences": { + "max": 17 + }, + "generatedWidgetPayload.missingCompatibilityCanonicalUnique": { + "max": 0 + }, + "generatedWidgetPayload.unexportedCompatibilityCanonicalUnique": { + "max": 0 + }, "colorDomainContracts.registeredUnique": { "max": 13 }, @@ -111,7 +138,7 @@ "max": 54 }, "colorDomainScopes.tokenContract.occurrences": { - "max": 268 + "max": 267 }, "colorDomainScopes.generatedWidget.occurrences": { "max": 0 diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index a17400fee..bbec90712 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -674,50 +674,7 @@ export const TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS = [ }, ]; -export const FALLBACK_VAR_CONTRACTS = [ - { - key: '--surface-stagger-index', - owner: 'src/web-ui/src/app/components/GalleryLayout', - reason: 'Runtime inline animation index with zero fallback for first paint and non-animated states.', - boundary: 'layout-runtime-input', - }, - { - key: '--mission-control-group-color', - owner: 'src/web-ui/src/app/components/panels/content-canvas/mission-control', - reason: 'Runtime group identity color with accent fallback when no group color is assigned.', - boundary: 'data-driven-identity-color', - }, - { - key: '--char-index', - owner: 'src/web-ui/src/component-library/components/StreamText', - reason: 'Runtime per-character animation offset with zero fallback outside animated rendering.', - boundary: 'animation-runtime-input', - }, - { - key: '--gallery-grid-min', - owner: 'src/web-ui/src/app/components/GalleryLayout', - reason: 'Runtime layout sizing input with a stable responsive grid fallback.', - boundary: 'layout-runtime-input', - }, - { - key: '--gallery-skeleton-height', - owner: 'src/web-ui/src/app/components/GalleryLayout', - reason: 'Runtime skeleton sizing input with a stable placeholder height fallback.', - boundary: 'layout-runtime-input', - }, - { - key: '--primary-color', - owner: 'src/web-ui/src/component-library/components/Markdown', - reason: 'Embedded markdown primary accent override with global accent fallback.', - boundary: 'embedded-content-theme-override', - }, - { - key: '--scene-viewport-border-width', - owner: 'src/web-ui/src/app/scenes/SceneViewport.scss', - reason: 'Runtime viewport layout override with a stable one-pixel default.', - boundary: 'layout-runtime-input', - }, -]; +export const FALLBACK_VAR_CONTRACTS = []; export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ { diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss index 5fafa6b5d..5515eab31 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss @@ -153,7 +153,7 @@ $content-max: 1480px; .gallery-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(var(--gallery-grid-min, 320px), 1fr)); + grid-template-columns: repeat(auto-fill, minmax(var(--gallery-grid-min), 1fr)); gap: $size-gap-3; align-content: start; @@ -165,14 +165,14 @@ $content-max: 1480px; } .gallery-skeleton-card { - height: var(--gallery-skeleton-height, 140px); + height: var(--gallery-skeleton-height); border-radius: $size-radius-lg; background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); position: relative; overflow: hidden; animation: gallery-item-in 0.28s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 60ms); + animation-delay: calc(var(--surface-stagger-index) * 60ms); &::after { content: ''; @@ -185,7 +185,7 @@ $content-max: 1480px; var(--skeleton-shimmer-peak) 50%, var(--skeleton-shimmer-0) 100% ); - animation: gallery-shimmer 1.6s ease-in-out calc(var(--surface-stagger-index, 0) * 0.12s) infinite; + animation: gallery-shimmer 1.6s ease-in-out calc(var(--surface-stagger-index) * 0.12s) infinite; } } diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss b/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss index 6f8530adf..6b78b46de 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/MissionControl.scss @@ -154,7 +154,7 @@ } &.is-active { - background: var(--mission-control-group-color, var(--color-accent-500)); + background: var(--mission-control-group-color); color: var(--color-text-primary); box-shadow: 0 1px 3px var(--color-overlay-black-20); @@ -170,7 +170,7 @@ width: 6px; height: 6px; border-radius: 50%; - background: var(--mission-control-group-color, var(--color-accent-500)); + background: var(--mission-control-group-color); opacity: 0.6; transition: opacity 0.15s ease; } diff --git a/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.scss b/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.scss index 85f113d6f..6e32f85c2 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/mission-control/ThumbnailCard.scss @@ -230,6 +230,8 @@ // Editor group badge &__group-badge { + --mission-control-group-color: var(--color-accent-500); + position: absolute; bottom: 6px; right: 6px; @@ -237,7 +239,7 @@ font-size: 9px; font-weight: 600; color: var(--color-text-primary); - background: var(--mission-control-group-color, var(--color-accent-500)); + background: var(--mission-control-group-color); border-radius: 3px; z-index: 1; box-shadow: 0 1px 3px var(--color-overlay-black-30); @@ -245,15 +247,15 @@ text-shadow: 0 1px 2px var(--color-overlay-black-20); &--primary { - background: var(--mission-control-group-color, var(--color-accent-500)); + --mission-control-group-color: var(--color-accent-500); } &--secondary { - background: var(--mission-control-group-color, var(--color-success)); + --mission-control-group-color: var(--color-success); } &--tertiary { - background: var(--mission-control-group-color, var(--color-warning)); + --mission-control-group-color: var(--color-warning); } } } diff --git a/src/web-ui/src/app/scenes/SceneViewport.scss b/src/web-ui/src/app/scenes/SceneViewport.scss index bc86e2eea..24e581b52 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.scss +++ b/src/web-ui/src/app/scenes/SceneViewport.scss @@ -11,7 +11,7 @@ min-height: 0; border-radius: $size-radius-base; border-style: solid; - border-width: var(--scene-viewport-border-width, 1px); + border-width: var(--scene-viewport-border-width); border-color: var(--border-subtle); background: var(--color-bg-scene); // Soft lift off workbench — layered shadow, kept subtle diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss index 8eed81218..28fae2765 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss @@ -11,7 +11,7 @@ overflow: hidden; cursor: pointer; animation: agent-card-in 0.22s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 35ms); + animation-delay: calc(var(--surface-stagger-index) * 35ms); transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease; diff --git a/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss b/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss index 967bb3de2..f3cc44b22 100644 --- a/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/_AgentSurfaceCard.scss @@ -13,7 +13,7 @@ overflow: hidden; cursor: pointer; animation: agent-surface-card-in 0.28s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 60ms); + animation-delay: calc(var(--surface-stagger-index) * 60ms); transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease; diff --git a/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss b/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss index 63c55ead5..852e2687e 100644 --- a/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss +++ b/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss @@ -11,7 +11,7 @@ overflow: hidden; cursor: pointer; animation: miniapp-card-in 0.22s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 35ms); + animation-delay: calc(var(--surface-stagger-index) * 35ms); transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease; @@ -92,7 +92,7 @@ &__icon { color: var(--color-text-primary); animation: miniapp-icon-pop 0.5s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 35ms + 60ms); + animation-delay: calc(var(--surface-stagger-index) * 35ms + 60ms); transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } diff --git a/src/web-ui/src/app/scenes/profile/views/NurseryView.scss b/src/web-ui/src/app/scenes/profile/views/NurseryView.scss index 45de62d54..7ec551453 100644 --- a/src/web-ui/src/app/scenes/profile/views/NurseryView.scss +++ b/src/web-ui/src/app/scenes/profile/views/NurseryView.scss @@ -292,7 +292,7 @@ $nursery-scene-gutter: clamp(40px, 6vw, 80px); text-align: left; font-family: $font-family-sans; animation: nursery-card-in 0.22s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 40ms); + animation-delay: calc(var(--surface-stagger-index) * 40ms); transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease; diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.scss b/src/web-ui/src/app/scenes/skills/SkillsScene.scss index 6054764d7..d611a8493 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.scss +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.scss @@ -654,7 +654,7 @@ background: var(--color-bg-secondary); cursor: pointer; animation: skills-card-in 0.18s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 30ms); + animation-delay: calc(var(--surface-stagger-index) * 30ms); transition: border-color $motion-fast $easing-standard, box-shadow $motion-fast $easing-standard, @@ -819,7 +819,7 @@ background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); animation: skills-card-in 0.2s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 40ms); + animation-delay: calc(var(--surface-stagger-index) * 40ms); } // ══════════════════════════════════════════════════════════════════════════════ @@ -998,7 +998,7 @@ background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); animation: skills-card-in 0.18s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 40ms); + animation-delay: calc(var(--surface-stagger-index) * 40ms); } // ══════════════════════════════════════════════════════════════════════════════ diff --git a/src/web-ui/src/app/scenes/skills/components/SkillCard.scss b/src/web-ui/src/app/scenes/skills/components/SkillCard.scss index 66e8b2136..c38886f6f 100644 --- a/src/web-ui/src/app/scenes/skills/components/SkillCard.scss +++ b/src/web-ui/src/app/scenes/skills/components/SkillCard.scss @@ -29,7 +29,7 @@ overflow: hidden; cursor: pointer; animation: skill-card-in 0.22s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 35ms); + animation-delay: calc(var(--surface-stagger-index) * 35ms); transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.35s ease; @@ -87,7 +87,7 @@ &__icon { color: var(--color-text-primary); animation: skill-card-icon-pop 0.46s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 35ms + 50ms); + animation-delay: calc(var(--surface-stagger-index) * 35ms + 50ms); } &__badges { diff --git a/src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss b/src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss index 164ed6359..59665753b 100644 --- a/src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss +++ b/src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss @@ -6,6 +6,7 @@ .base-tool-card { --primary-color: #{tokens.$color-accent-600}; + --markdown-primary-color: var(--primary-color); @include shared.liquid-card-base; @include shared.card-light-flow; diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.scss b/src/web-ui/src/component-library/components/Markdown/Markdown.scss index 6c30d58e9..b967617ab 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.scss +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.scss @@ -32,7 +32,6 @@ --markdown-table-header-bg: var(--flowchat-md-table-th-bg); --markdown-table-row-stripe: var(--flowchat-md-table-row-stripe); --markdown-table-row-hover: var(--flowchat-md-table-hover); - --markdown-primary-color: var(--primary-color, var(--color-accent-600)); --markdown-link-color: var(--flowchat-link-color); --markdown-link-hover-color: var(--flowchat-link-hover-color); diff --git a/src/web-ui/src/component-library/components/StreamText/StreamText.scss b/src/web-ui/src/component-library/components/StreamText/StreamText.scss index a3da90ff4..4d2260069 100644 --- a/src/web-ui/src/component-library/components/StreamText/StreamText.scss +++ b/src/web-ui/src/component-library/components/StreamText/StreamText.scss @@ -1,5 +1,6 @@ /* Streaming text output styles */ .stream-text { + --char-index: 0; --stream-text-matrix-green-rgb: 0, 255, 0; --stream-text-rainbow-pink-rgb: 255, 0, 128; --stream-text-rainbow-orange-rgb: 255, 140, 0; @@ -152,16 +153,16 @@ @keyframes glitch-appear { 0% { opacity: 0; - transform: translate(calc(var(--char-index, 0) * -2px), 0); + transform: translate(calc(var(--char-index) * -2px), 0); text-shadow: 2px 0 var(--stream-text-glitch-magenta), -2px 0 var(--stream-text-glitch-cyan); } 20% { - transform: translate(calc(var(--char-index, 0) * 2px), -2px); + transform: translate(calc(var(--char-index) * 2px), -2px); } 40% { - transform: translate(calc(var(--char-index, 0) * -1px), 2px); + transform: translate(calc(var(--char-index) * -1px), 2px); } 60% { text-shadow: diff --git a/src/web-ui/src/component-library/styles/tokens.scss b/src/web-ui/src/component-library/styles/tokens.scss index 93e8800e7..bc2c3ca73 100644 --- a/src/web-ui/src/component-library/styles/tokens.scss +++ b/src/web-ui/src/component-library/styles/tokens.scss @@ -1036,7 +1036,7 @@ $badge-info-text: $color-info; --mission-control-group-tertiary-color: var(--color-warning); --deep-review-consent-accent: #2563eb; --deep-review-consent-accent-strong: #1d4ed8; - --deep-review-consent-accent-strong-dark: #2563eb; + --deep-review-consent-accent-strong-dark: var(--deep-review-consent-accent); --deep-review-consent-warm-dark: #fbbf24; --component-action-danger-rgb: 199, 112, 112; --component-action-success-rgb: 110, 184, 140; @@ -1212,6 +1212,9 @@ $badge-info-text: $color-info; --surface-stagger-index: 0; --mission-control-group-color: var(--color-accent-500); + --scene-viewport-border-width: 1px; + --gallery-grid-min: 320px; + --gallery-skeleton-height: 140px; --inline-context-tag-color: var(--color-accent-500); --deep-review-action-bar-surface: var(--color-bg-secondary); --markdown-editor-list-indent: 1.75rem; @@ -1417,6 +1420,7 @@ $badge-info-text: $color-info; --flowchat-markdown-table-cell-pad-x: 0.72rem; --flowchat-link-color: #60a5fa; --flowchat-link-hover-color: #93c5fd; + --markdown-primary-color: var(--color-accent-600); /* Left nav: one baseline step below flow-chat scale (overwritten by FontPreferenceService). */ --nav-font-size-xxs: var(--font-size-xxs); diff --git a/src/web-ui/src/tools/generative-widget/themePayload.test.ts b/src/web-ui/src/tools/generative-widget/themePayload.test.ts index c275d30f4..92b0b0128 100644 --- a/src/web-ui/src/tools/generative-widget/themePayload.test.ts +++ b/src/web-ui/src/tools/generative-widget/themePayload.test.ts @@ -7,7 +7,7 @@ import { readWidgetThemePayload, } from './themePayload'; -const WIDGET_THEME_VAR_NAMES_HASH = '3234a6b63576f03b4a7e3f97349f6a042fffaf6300943e5f8972eecdb5347683'; +const WIDGET_THEME_VAR_NAMES_HASH = '703944f63b95646520792c18ee2978a5d10553145a62ee53980aea2ca98baa0b'; function readPayloadWithHostValues(hostValues: Record = {}) { const requestedNames: string[] = []; @@ -60,7 +60,7 @@ describe('generated widget theme payload contract', () => { first: requestedNames[0], last: requestedNames[requestedNames.length - 1], }).toEqual({ - count: 324, + count: 326, hash: WIDGET_THEME_VAR_NAMES_HASH, first: '--color-bg-primary', last: '--tool-card-action-font-weight', diff --git a/src/web-ui/src/tools/generative-widget/themePayload.ts b/src/web-ui/src/tools/generative-widget/themePayload.ts index 323f37832..a6c3b4c21 100644 --- a/src/web-ui/src/tools/generative-widget/themePayload.ts +++ b/src/web-ui/src/tools/generative-widget/themePayload.ts @@ -261,6 +261,8 @@ const WIDGET_THEME_VAR_GROUPS = { '--size-radius-md', '--size-radius-lg', '--size-radius-xl', + '--size-radius-2xl', + '--size-radius-full', '--spacing-1', '--spacing-2', '--spacing-3',