From ed2b47fe906767c1c4f745c12432d737cd1fd70a Mon Sep 17 00:00:00 2001 From: leefer Date: Wed, 29 Jul 2026 21:56:39 +0800 Subject: [PATCH] refactor: govern CSS tokens and cascade --- docs/governance/architecture-inventory.json | 27 +- docs/governance/stage-16-css-tokens.md | 49 +++ static/design-system.css | 43 --- static/index.html | 1 + static/redesign-v2.css | 46 --- static/renovation.css | 83 ---- static/shared/tokens.css | 402 ++++++++++++++++++++ static/styles.css | 84 ---- static/theme.css | 156 +------- tests/test_css_governance.py | 82 ++++ tests/test_frontend_contract.py | 3 +- 11 files changed, 551 insertions(+), 425 deletions(-) create mode 100644 docs/governance/stage-16-css-tokens.md create mode 100644 static/shared/tokens.css create mode 100644 tests/test_css_governance.py diff --git a/docs/governance/architecture-inventory.json b/docs/governance/architecture-inventory.json index a8cc486..2d5102d 100644 --- a/docs/governance/architecture-inventory.json +++ b/docs/governance/architecture-inventory.json @@ -244,6 +244,7 @@ } ], "css_layers": [ + "/shared/tokens.css?v=20260729-1", "/styles.css", "/renovation.css?v=20260725-5", "/redesign-v2.css?v=20260728-1", @@ -259,23 +260,23 @@ }, { "path": "static/styles.css", - "bytes": 364385, - "lines": 15549 - }, - { - "path": "static/redesign-v2.css", - "bytes": 264960, - "lines": 8616 + "bytes": 361780, + "lines": 15465 }, { "path": "server.py", "bytes": 263744, "lines": 5856 }, + { + "path": "static/redesign-v2.css", + "bytes": 263539, + "lines": 8570 + }, { "path": "static/index.html", - "bytes": 133815, - "lines": 1875 + "bytes": 133880, + "lines": 1876 }, { "path": "database.py", @@ -294,8 +295,8 @@ }, { "path": "static/renovation.css", - "bytes": 86245, - "lines": 1636 + "bytes": 83949, + "lines": 1553 }, { "path": "static/wentian-v2.css", @@ -309,8 +310,8 @@ }, { "path": "static/theme.css", - "bytes": 41279, - "lines": 1407 + "bytes": 36427, + "lines": 1253 } ] } diff --git a/docs/governance/stage-16-css-tokens.md b/docs/governance/stage-16-css-tokens.md new file mode 100644 index 0000000..099ef9b --- /dev/null +++ b/docs/governance/stage-16-css-tokens.md @@ -0,0 +1,49 @@ +# Stage 16: CSS Tokens and Cascade Governance + +## Scope + +This stage changes CSS ownership, not visual design. Existing selectors, page geometry, +responsive behavior, theme behavior, and the isolated Wentian visual layer remain intact. + +## Canonical Token Layer + +`static/shared/tokens.css` is the only owner of global application tokens. It is loaded before +all application styles and follows a three-layer contract: + +1. Primitive tokens contain raw palette, dimension, elevation, and motion values. +2. Semantic tokens describe interface meaning such as canvas, surface, border, text, action, + market direction, and warning states. +3. Component tokens define shared card, control, shell, table, chart, and profile contracts. + +Dark mode overrides semantic and compatibility values in the same file. It does not redefine +component geometry. The `--wt-*` namespace remains owned by `wentian-v2.css` because Wentian +has a deliberately isolated visual language. + +## Compatibility + +The historical `--xb-*`, `--r2-*`, and flat variables such as `--blue`, `--line`, and `--up` +remain compatibility aliases. Their effective light and dark values are unchanged. New CSS +must use semantic or component tokens; compatibility names exist only so feature selectors can +be migrated incrementally without a broad visual rewrite. + +The stylesheet order is fixed as: + +1. shared tokens; +2. legacy/base styles; +3. renovation and page redesign rules; +4. canonical non-Wentian design-system rules; +5. runtime theme selectors; +6. isolated Wentian rules. + +## Enforcement + +`tests/test_css_governance.py` verifies the load order, the three token layers, single global +ownership, compatibility coverage, and Wentian isolation. A new global `:root` token block in +any legacy stylesheet fails the regression suite. + +## Residual Migration + +Page styles still contain literal values and historical variable references. Removing those is +an incremental page-by-page task because blindly replacing them would risk changing already +accepted visuals. Stage 16 establishes the ownership boundary that prevents new drift while +leaving safe migration points for later stages. diff --git a/static/design-system.css b/static/design-system.css index 2770c65..dcc452c 100644 --- a/static/design-system.css +++ b/static/design-system.css @@ -1,48 +1,5 @@ /* Canonical non-heaven design system. Question-to-Heaven remains isolated. */ /* ========== 小白复盘 · 打样设计系统 ========== */ -:root{ - --blue:#2563eb; --blue-d:#1d4ed8; --blue-soft:#eff4ff; --blue-line:#c7d8fb; - --up:#e04536; --up-soft:#fdecea; --down:#16a34a; --down-soft:#e9f7ee; - --amber:#b45309; --amber-soft:#fdf3e3; - --ink:#1f2937; --sub:#6b7280; --faint:#9ca3af; - --line:#e5e7eb; --line-soft:#eef0f3; --bg:#f4f5f7; --card:#fff; - --radius:10px; - --shadow:0 1px 2px rgba(16,24,40,.05); - /* Shell dimensions are tokens because data-heavy pages reuse them in viewport calculations. */ - --sidebar-width:200px; - --topbar-height:46px; - --summary-height:34px; - --statusbar-height:30px; - --page-pad-y:14px; - --page-pad-x:16px; - --card-gap:12px; - --workspace-height:calc(100vh - var(--topbar-height) - var(--statusbar-height)); - --content-height:calc(var(--workspace-height) - var(--summary-height)); - --table-wide:1180px; - --table-medium:930px; - --table-compact:720px; - --col-rank:44px; - --col-date:94px; - --col-stock:160px; - --col-number:96px; - --col-action:96px; - --col-text:220px; - --right-rail-wide:372px; - --pool-table-max-height:calc(var(--content-height) - var(--topbar-height) - var(--page-pad-y) - var(--page-pad-y) - var(--card-gap)); - --sentiment-history-max-height:510px; - --sentiment-history-min-height:220px; - --primary-share:1.45fr; - --secondary-share:.75fr; - --mobile-nav-height:58px; - --mobile-header-height:50px; - --mobile-tab-height:54px; - --mobile-shell-pad:8px; - --mobile-page-pad:10px; - --mobile-min-width:320px; - --space-4:4px; - --font-aux:10.5px; -} - *{box-sizing:border-box;margin:0;padding:0} html,body{height:100%} body{ diff --git a/static/index.html b/static/index.html index ecfdf8c..cdcea87 100644 --- a/static/index.html +++ b/static/index.html @@ -17,6 +17,7 @@ document.documentElement.style.colorScheme = theme; })(); + diff --git a/static/redesign-v2.css b/static/redesign-v2.css index b4b7bac..1460dc6 100644 --- a/static/redesign-v2.css +++ b/static/redesign-v2.css @@ -3,52 +3,6 @@ * Values and composition are transferred from the approved prototypes in * ../界面优化. Keep later page migrations in this file until stage 19 cleanup. */ -:root { - --r2-blue: #2563eb; - --r2-blue-dark: #1d4ed8; - --r2-blue-soft: #eff4ff; - --r2-blue-line: #c7d8fb; - --r2-up: #e04536; - --r2-up-soft: #fdecea; - --r2-down: #16a34a; - --r2-down-soft: #e9f7ee; - --r2-amber: #b45309; - --r2-amber-soft: #fdf3e3; - --r2-ink: #1f2937; - --r2-sub: #6b7280; - --r2-faint: #9ca3af; - --r2-line: #e5e7eb; - --r2-line-soft: #eef0f3; - --r2-bg: #f4f5f7; - --r2-card: #fff; - --r2-radius: 10px; - --r2-shadow: 0 1px 2px rgba(16, 24, 40, .05); - /* Dragon profile component tokens. */ - --dragon-profile-list-width: 340px; - --dragon-profile-detail-min-height: 460px; - --dragon-profile-list-max-height: 320px; - --dragon-profile-row-min-height: 64px; - --dragon-profile-row-avatar-size: 36px; - --dragon-profile-avatar-size: 72px; - --dragon-profile-control-height: 33px; - --dragon-profile-gap: 12px; - --dragon-profile-panel-padding: 16px; - --dragon-profile-row-padding: 10px 12px; - --dragon-profile-title-font: 18px; - --dragon-profile-name-font: 13px; - --dragon-profile-body-font: 12px; - --dragon-profile-meta-font: 11px; - --dragon-profile-transition: 160ms ease; - --dragon-profile-border-width: 1px; - --dragon-profile-focus-width: 2px; - --dragon-profile-focus-offset: -2px; - --dragon-profile-radius-inset: 2px; - --dragon-profile-body-line-height: 1.75; - --dragon-profile-icon-stroke: 1; - --dragon-profile-weight-strong: 750; - --dragon-profile-weight-semibold: 600; -} - html, body { min-height: 100%; diff --git a/static/renovation.css b/static/renovation.css index 4df2cd3..f6b23b0 100644 --- a/static/renovation.css +++ b/static/renovation.css @@ -2,60 +2,6 @@ Structural layer built on semantic tokens. Page-specific character (notably Wentian) remains owned by the underlying feature styles. */ -:root { - /* primitive */ - --xb-gray-25: #fcfcfd; - --xb-gray-50: #f8f9fb; - --xb-gray-100: #f2f4f7; - --xb-gray-200: #e5e8ee; - --xb-gray-300: #d4d9e2; - --xb-gray-500: #697386; - --xb-gray-700: #344054; - --xb-gray-900: #172033; - --xb-blue-50: #eef4ff; - --xb-blue-100: #dce8ff; - --xb-blue-500: #356ae6; - --xb-blue-600: #2858c7; - --xb-red-50: #fff1f0; - --xb-red-500: #df493b; - --xb-green-50: #ecf9f1; - --xb-green-500: #179b55; - --xb-amber-50: #fff7e8; - --xb-amber-500: #d7820b; - - /* semantic */ - --surface-canvas: var(--xb-gray-100); - --surface-raised: #ffffff; - --surface-subtle: var(--xb-gray-50); - --surface-selected: var(--xb-blue-50); - --text-primary: var(--xb-gray-900); - --text-secondary: var(--xb-gray-500); - --text-tertiary: #98a1b2; - --border: var(--xb-gray-200); - --border-strong: var(--xb-gray-300); - --primary: var(--xb-blue-500); - --primary-hover: var(--xb-blue-600); - --danger: var(--xb-red-500); - --success: var(--xb-green-500); - --warning: var(--xb-amber-500); - --radius-sm: 5px; - --radius-md: 8px; - --radius-lg: 10px; - --shadow-xs: 0 1px 2px rgba(16, 24, 40, .04); - --shadow-sm: 0 3px 10px rgba(16, 24, 40, .055); - --shadow-float: 0 14px 38px rgba(16, 24, 40, .14); - --duration-fast: 150ms; - --duration-normal: 220ms; - - /* component */ - --card-bg: var(--surface-raised); - --card-border: var(--border); - --card-radius: var(--radius-lg); - --card-shadow: var(--shadow-xs); - --control-height: 34px; - --page-gap: 12px; -} - html { background: var(--surface-canvas); } body { @@ -870,35 +816,6 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view { .curated-strategy-card { animation: none !important; transition: none !important; } } -/* Visual master alignment: based on the approved desktop prototypes. */ -:root { - --xb-blue-500: #2563eb; - --xb-blue-600: #1d4ed8; - --xb-red-500: #e04536; - --xb-green-500: #16a34a; - --xb-amber-500: #b45309; - --surface-canvas: #f4f5f7; - --surface-subtle: #f8fafc; - --text-primary: #1f2937; - --text-secondary: #6b7280; - --text-tertiary: #9ca3af; - --border: #e5e7eb; - --border-strong: #d1d5db; - --primary: var(--xb-blue-500); - --primary-hover: var(--xb-blue-600); - --danger: var(--xb-red-500); - --success: var(--xb-green-500); - --warning: var(--xb-amber-500); - --radius-sm: 5px; - --radius-md: 7px; - --radius-lg: 10px; - --shadow-xs: 0 1px 2px rgba(16, 24, 40, .05); - --shadow-sm: 0 4px 14px rgba(16, 24, 40, .06); - --card-radius: 10px; - --card-shadow: var(--shadow-xs); - --control-height: 32px; -} - body { grid-template-columns: 200px minmax(0, 1fr); grid-template-rows: 46px minmax(0, 1fr) 30px; diff --git a/static/shared/tokens.css b/static/shared/tokens.css new file mode 100644 index 0000000..a600fea --- /dev/null +++ b/static/shared/tokens.css @@ -0,0 +1,402 @@ +/* + * Canonical frontend tokens. + * + * Ownership flows in one direction: + * primitive values -> semantic meaning -> component contracts. + * Historical variable names remain aliases so page CSS can migrate without + * changing the rendered interface. + */ + +:root { + color-scheme: light; + + /* Primitive tokens */ + --color-white: #ffffff; + --color-gray-25: #fcfcfd; + --color-gray-50: #f8f9fb; + --color-gray-100: #f2f4f7; + --color-gray-200: #e5e8ee; + --color-gray-300: #d4d9e2; + --color-gray-500: #697386; + --color-gray-700: #344054; + --color-gray-900: #172033; + --color-shell-canvas: #f1f4f6; + --color-page-canvas: #f4f5f7; + --color-surface-muted: #f6f8fa; + --color-surface-subtle: #f8fafc; + --color-border: #e5e7eb; + --color-border-strong: #d1d5db; + --color-text-primary: #1f2937; + --color-text-secondary: #6b7280; + --color-text-tertiary: #9ca3af; + --color-action-base: #1769c2; + --color-action-base-hover: #10569f; + --color-action-base-soft: #eaf2fb; + --color-action: #2563eb; + --color-action-hover: #1d4ed8; + --color-action-soft: #eff4ff; + --color-action-line: #c7d8fb; + --color-market-up-base: #d33f49; + --color-market-up-base-soft: #fff0f1; + --color-market-up: #e04536; + --color-market-up-soft: #fdecea; + --color-market-down-base: #07805b; + --color-market-down-base-soft: #eaf7f2; + --color-market-down: #16a34a; + --color-market-down-soft: #e9f7ee; + --color-warning-base: #aa6800; + --color-warning-base-soft: #fff6e5; + --color-warning: #b45309; + --color-warning-soft: #fdf3e3; + + --size-radius-sm: 5px; + --size-radius-md: 7px; + --size-radius-lg: 10px; + --size-control: 32px; + --size-sidebar: 200px; + --size-topbar: 46px; + --size-summary: 34px; + --size-statusbar: 30px; + --size-page-pad-y: 14px; + --size-page-pad-x: 16px; + --size-card-gap: 12px; + + --elevation-card: 0 1px 2px rgba(16, 24, 40, .05); + --elevation-soft: 0 1px 2px rgba(22, 34, 46, .04), 0 5px 18px rgba(22, 34, 46, .035); + --elevation-raised: 0 4px 14px rgba(16, 24, 40, .06); + --elevation-float: 0 14px 38px rgba(16, 24, 40, .14); + --motion-instant: 100ms; + --motion-fast: 140ms; + --motion-medium: 200ms; + --motion-deliberate: 260ms; + --motion-slow: 560ms; + --ease-out: cubic-bezier(0.22, 1, 0.36, 1); + + /* Semantic tokens */ + --canvas: var(--color-shell-canvas); + --surface: var(--color-white); + --surface-muted: var(--color-surface-muted); + --surface-subtle: var(--color-surface-subtle); + --surface-canvas: var(--color-page-canvas); + --surface-raised: var(--color-white); + --surface-selected: #eef4ff; + --border: var(--color-border); + --border-strong: var(--color-border-strong); + --text-primary: var(--color-text-primary); + --text-secondary: var(--color-text-secondary); + --text-tertiary: var(--color-text-tertiary); + --action: var(--color-action-base); + --action-hover: var(--color-action-base-hover); + --action-soft: var(--color-action-base-soft); + --market-up: var(--color-market-up-base); + --market-up-soft: var(--color-market-up-base-soft); + --market-down: var(--color-market-down-base); + --market-down-soft: var(--color-market-down-base-soft); + --warning-color: var(--color-warning-base); + --warning-soft: var(--color-warning-base-soft); + --primary: var(--color-action); + --primary-hover: var(--color-action-hover); + --danger: var(--color-market-up); + --success: var(--color-market-down); + --warning: var(--color-warning); + + /* Component tokens */ + --card-bg: var(--surface-raised); + --card-border: var(--border); + --card-radius: var(--size-radius-lg); + --card-shadow: var(--elevation-card); + --control-height: var(--size-control); + --page-gap: var(--size-card-gap); + --radius-sm: var(--size-radius-sm); + --radius-md: var(--size-radius-md); + --radius-lg: var(--size-radius-lg); + --shadow-xs: var(--elevation-card); + --shadow-sm: var(--elevation-raised); + --shadow-float: var(--elevation-float); + --duration-fast: 150ms; + --duration-normal: 220ms; + + --sidebar-width: var(--size-sidebar); + --topbar-height: var(--size-topbar); + --summary-height: var(--size-summary); + --statusbar-height: var(--size-statusbar); + --page-pad-y: var(--size-page-pad-y); + --page-pad-x: var(--size-page-pad-x); + --card-gap: var(--size-card-gap); + --workspace-height: calc(100vh - var(--topbar-height) - var(--statusbar-height)); + --content-height: calc(var(--workspace-height) - var(--summary-height)); + --table-wide: 1180px; + --table-medium: 930px; + --table-compact: 720px; + --col-rank: 44px; + --col-date: 94px; + --col-stock: 160px; + --col-number: 96px; + --col-action: 96px; + --col-text: 220px; + --right-rail-wide: 372px; + --pool-table-max-height: calc(var(--content-height) - var(--topbar-height) - var(--page-pad-y) - var(--page-pad-y) - var(--card-gap)); + --sentiment-history-max-height: 510px; + --sentiment-history-min-height: 220px; + --primary-share: 1.45fr; + --secondary-share: .75fr; + --mobile-nav-height: 58px; + --mobile-header-height: 50px; + --mobile-tab-height: 54px; + --mobile-shell-pad: 8px; + --mobile-page-pad: 10px; + --mobile-min-width: 320px; + --space-4: 4px; + --font-aux: 10.5px; + + --dragon-profile-list-width: 340px; + --dragon-profile-detail-min-height: 460px; + --dragon-profile-list-max-height: 320px; + --dragon-profile-row-min-height: 64px; + --dragon-profile-row-avatar-size: 36px; + --dragon-profile-avatar-size: 72px; + --dragon-profile-control-height: 33px; + --dragon-profile-gap: 12px; + --dragon-profile-panel-padding: 16px; + --dragon-profile-row-padding: 10px 12px; + --dragon-profile-title-font: 18px; + --dragon-profile-name-font: 13px; + --dragon-profile-body-font: 12px; + --dragon-profile-meta-font: 11px; + --dragon-profile-transition: 160ms ease; + --dragon-profile-border-width: 1px; + --dragon-profile-focus-width: 2px; + --dragon-profile-focus-offset: -2px; + --dragon-profile-radius-inset: 2px; + --dragon-profile-body-line-height: 1.75; + --dragon-profile-icon-stroke: 1; + --dragon-profile-weight-strong: 750; + --dragon-profile-weight-semibold: 600; + + --chart-background: #fbfcfd; + --chart-grid: #e2e8ec; + --chart-axis: #6c7983; + --chart-zero: #aeb7c1; + --chart-line: #1d65c1; + --chart-average: #b7791f; + --chart-up: #c93f45; + --chart-down: #087a55; + --chart-up-volume: rgba(201, 63, 69, .58); + --chart-down-volume: rgba(8, 122, 85, .58); + --chart-area: rgba(37, 99, 235, .07); + --chart-alert-area: rgba(224, 69, 54, .05); + --chart-moving-average: #d1d5db; + --chart-repair: #f59e0b; + --chart-ma-10: #a76500; + --chart-ma-20: #626c78; + + /* Compatibility aliases. Do not add new usage of these names. */ + --xb-gray-25: var(--color-gray-25); + --xb-gray-50: var(--color-gray-50); + --xb-gray-100: var(--color-gray-100); + --xb-gray-200: var(--color-gray-200); + --xb-gray-300: var(--color-gray-300); + --xb-gray-500: var(--color-gray-500); + --xb-gray-700: var(--color-gray-700); + --xb-gray-900: var(--color-gray-900); + --xb-blue-50: #eef4ff; + --xb-blue-100: #dce8ff; + --xb-blue-500: var(--color-action); + --xb-blue-600: var(--color-action-hover); + --xb-red-50: #fff1f0; + --xb-red-500: var(--color-market-up); + --xb-green-50: #ecf9f1; + --xb-green-500: var(--color-market-down); + --xb-amber-50: #fff7e8; + --xb-amber-500: var(--color-warning); + + --bg: var(--surface-canvas); + --card: var(--surface-raised); + --ink: var(--text-primary); + --sub: var(--text-secondary); + --faint: var(--text-tertiary); + --line: var(--border); + --line-soft: #eef0f3; + --line-strong: var(--border-strong); + --text: var(--text-primary); + --text-muted: var(--text-secondary); + --blue: var(--primary); + --blue-d: var(--primary-hover); + --blue-dark: var(--action-hover); + --blue-soft: var(--color-action-soft); + --blue-line: var(--color-action-line); + --up: var(--danger); + --up-soft: var(--color-market-up-soft); + --down: var(--success); + --down-soft: var(--color-market-down-soft); + --coral: var(--market-up); + --coral-soft: var(--market-up-soft); + --green: var(--market-down); + --green-soft: var(--market-down-soft); + --amber: var(--color-warning); + --amber-soft: var(--color-warning-soft); + --radius: var(--size-radius-lg); + --shadow: var(--elevation-card); + --shadow-soft: var(--elevation-soft); + + --r2-blue: var(--primary); + --r2-blue-dark: var(--primary-hover); + --r2-blue-soft: var(--color-action-soft); + --r2-blue-line: var(--color-action-line); + --r2-up: var(--danger); + --r2-up-soft: var(--color-market-up-soft); + --r2-down: var(--success); + --r2-down-soft: var(--color-market-down-soft); + --r2-amber: var(--color-warning); + --r2-amber-soft: var(--color-warning-soft); + --r2-ink: var(--text-primary); + --r2-sub: var(--text-secondary); + --r2-faint: var(--text-tertiary); + --r2-line: var(--border); + --r2-line-soft: #eef0f3; + --r2-bg: var(--surface-canvas); + --r2-card: var(--surface-raised); + --r2-radius: var(--size-radius-lg); + --r2-shadow: var(--elevation-card); + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif; + font-size: 14px; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --canvas: #121416; + --surface: #1b1e21; + --surface-muted: #202428; + --surface-subtle: #24282d; + --surface-canvas: var(--canvas); + --surface-raised: var(--surface); + --surface-selected: #23364a; + --border: #343a40; + --border-strong: #474f57; + --text-primary: #e8eaed; + --text-secondary: #adb5bd; + --text-tertiary: #7f8993; + --action: #6ca8e8; + --action-hover: #8bbcf0; + --action-soft: #23364a; + --market-up: #f06d73; + --market-up-soft: #40262a; + --market-down: #43bc8a; + --market-down-soft: #1d382f; + --warning-color: #e2ad58; + --warning-soft: #3d3220; + --primary: var(--action); + --primary-hover: var(--action-hover); + --danger: var(--market-up); + --success: var(--market-down); + --warning: var(--warning-color); + --card-bg: var(--surface); + --card-border: var(--border); + + --xb-gray-25: var(--surface); + --xb-gray-50: var(--surface-muted); + --xb-gray-100: var(--canvas); + --xb-gray-200: var(--border); + --xb-gray-300: var(--border-strong); + --xb-gray-500: var(--text-secondary); + --xb-gray-700: #cbd1d7; + --xb-gray-900: var(--text-primary); + --xb-blue-50: var(--action-soft); + --xb-blue-100: #294866; + --xb-blue-500: var(--action); + --xb-blue-600: var(--action-hover); + --xb-red-50: var(--market-up-soft); + --xb-red-500: var(--market-up); + --xb-green-50: var(--market-down-soft); + --xb-green-500: var(--market-down); + --xb-amber-50: var(--warning-soft); + --xb-amber-500: var(--warning-color); + + --bg: var(--canvas); + --card: var(--surface); + --ink: var(--text-primary); + --sub: var(--text-secondary); + --faint: var(--text-tertiary); + --line: var(--border); + --line-soft: #2a2f34; + --line-strong: var(--border-strong); + --text: var(--text-primary); + --text-muted: var(--text-secondary); + --blue: var(--action); + --blue-d: var(--action-hover); + --blue-dark: var(--action-hover); + --blue-soft: var(--action-soft); + --blue-line: #42698e; + --up: var(--market-up); + --up-soft: var(--market-up-soft); + --down: var(--market-down); + --down-soft: var(--market-down-soft); + --coral: var(--market-up); + --coral-soft: var(--market-up-soft); + --green: var(--market-down); + --green-soft: var(--market-down-soft); + --amber: var(--warning-color); + --amber-soft: var(--warning-soft); + --r2-blue: var(--action); + --r2-blue-dark: var(--action-hover); + --r2-blue-soft: var(--action-soft); + --r2-blue-line: #42698e; + --r2-up: var(--market-up); + --r2-up-soft: var(--market-up-soft); + --r2-down: var(--market-down); + --r2-down-soft: var(--market-down-soft); + --r2-amber: var(--warning-color); + --r2-amber-soft: var(--warning-soft); + --r2-ink: var(--text-primary); + --r2-sub: var(--text-secondary); + --r2-faint: var(--text-tertiary); + --r2-line: var(--border); + --r2-line-soft: #2a2f34; + --r2-bg: var(--canvas); + --r2-card: var(--surface); + --r2-shadow: var(--shadow-soft); + + --dialog-ink: var(--text-primary); + --dialog-line: var(--border); + --heaven-paper: #191a18; + --heaven-paper-soft: #20211e; + --heaven-ink: #e5e0d4; + --heaven-muted: #aaa497; + --heaven-rule: #3d3a34; + --chart-background: #181b1e; + --chart-grid: #30363c; + --chart-axis: #a3adb6; + --chart-zero: #68737d; + --chart-line: #6ca8e8; + --chart-average: #e2ad58; + --chart-up: #f06d73; + --chart-down: #43bc8a; + --chart-up-volume: rgba(240, 109, 115, .52); + --chart-down-volume: rgba(67, 188, 138, .52); + --chart-area: rgba(108, 168, 232, .12); + --chart-alert-area: rgba(240, 109, 115, .09); + --chart-moving-average: #69737d; + --chart-repair: #e2ad58; + --chart-ma-10: #d39a45; + --chart-ma-20: #9aa5af; + --on-action: #101418; + --warning-line: #6d5a38; + --warning-line-strong: #66502d; + --control-shadow: 0 1px 3px rgba(0, 0, 0, .3); + --dialog-backdrop: rgba(0, 0, 0, .62); + --ladder-level-1: #2d2426; + --ladder-level-2: #2b2822; + --ladder-level-3: #252a2d; + --ladder-level-4: #20282b; + --ladder-level-5: #202428; + --heat-strong-bg: #304f7a; + --heat-strong-ink: #f2f6fb; + --heat-warm-bg: #2b405f; + --heat-warm-ink: #dfeaf7; + --heat-mild-bg: #293440; + --heat-mild-ink: #c7d2dc; + --heaven-field-bg: #23241f; + --shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16); + --shadow: 0 18px 50px rgba(0, 0, 0, .46); +} diff --git a/static/styles.css b/static/styles.css index 375e419..ae4c0fb 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,47 +1,3 @@ -:root { - color-scheme: light; - --canvas: #f5f6f8; - --surface: #ffffff; - --surface-muted: #f8f9fb; - --border: #e1e6eb; - --border-strong: #c7d0d9; - --text-primary: #171a1f; - --text-secondary: #626c78; - --action: #1d65c1; - --action-hover: #164f99; - --action-soft: #eaf2fc; - --market-up: #c93f45; - --market-up-soft: #fcecee; - --market-down: #087a55; - --market-down-soft: #e8f6f0; - --warning-color: #a76500; - --warning-soft: #fff4df; - --bg: var(--canvas); - --line: var(--border); - --line-strong: var(--border-strong); - --text: var(--text-primary); - --text-muted: var(--text-secondary); - --blue: var(--action); - --blue-dark: var(--action-hover); - --blue-soft: var(--action-soft); - --coral: var(--market-up); - --coral-soft: var(--market-up-soft); - --green: var(--market-down); - --green-soft: var(--market-down-soft); - --amber: var(--warning-color); - --amber-soft: var(--warning-soft); - --shadow: 0 14px 36px rgba(24, 34, 45, 0.14); - --shadow-soft: 0 2px 8px rgba(24, 34, 45, 0.055); - --motion-instant: 100ms; - --motion-fast: 140ms; - --motion-medium: 200ms; - --motion-deliberate: 260ms; - --motion-slow: 560ms; - --ease-out: cubic-bezier(0.22, 1, 0.36, 1); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif; - font-size: 14px; -} - /* Strategy library and quantitative screener */ .screener-mode-tabs { min-height: 48px; @@ -13844,46 +13800,6 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin #screenerView .screener-result-frame tbody tr td { transition: none; } } -/* Unified visual system 2026-07 */ -:root { - --canvas: #f1f4f6; - --surface: #ffffff; - --surface-muted: #f6f8fa; - --surface-subtle: #fafbfc; - --border: #dfe4e9; - --border-strong: #cbd3dc; - --text-primary: #17202a; - --text-secondary: #607080; - --text-tertiary: #8995a2; - --action: #1769c2; - --action-hover: #10569f; - --action-soft: #eaf2fb; - --market-up: #d33f49; - --market-up-soft: #fff0f1; - --market-down: #07805b; - --market-down-soft: #eaf7f2; - --warning-color: #aa6800; - --warning-soft: #fff6e5; - --bg: var(--canvas); - --line: var(--border); - --line-strong: var(--border-strong); - --text: var(--text-primary); - --text-muted: var(--text-secondary); - --blue: var(--action); - --blue-dark: var(--action-hover); - --blue-soft: var(--action-soft); - --coral: var(--market-up); - --coral-soft: var(--market-up-soft); - --green: var(--market-down); - --green-soft: var(--market-down-soft); - --amber: var(--warning-color); - --amber-soft: var(--warning-soft); - --shadow-soft: 0 1px 2px rgba(22, 34, 46, .04), 0 5px 18px rgba(22, 34, 46, .035); - --shadow: 0 18px 50px rgba(22, 34, 46, .18); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif; - font-size: 14px; -} - html, body { background: var(--canvas); diff --git a/static/theme.css b/static/theme.css index 1ddb7b0..08d117f 100644 --- a/static/theme.css +++ b/static/theme.css @@ -1,22 +1,4 @@ -/* Runtime theme layer. Existing page geometry stays untouched; only semantic color tokens change. */ -:root { - --chart-background: #fbfcfd; - --chart-grid: #e2e8ec; - --chart-axis: #6c7983; - --chart-zero: #aeb7c1; - --chart-line: #1d65c1; - --chart-average: #b7791f; - --chart-up: #c93f45; - --chart-down: #087a55; - --chart-up-volume: rgba(201, 63, 69, .58); - --chart-down-volume: rgba(8, 122, 85, .58); - --chart-area: rgba(37, 99, 235, .07); - --chart-alert-area: rgba(224, 69, 54, .05); - --chart-moving-average: #d1d5db; - --chart-repair: #f59e0b; - --chart-ma-10: #a76500; - --chart-ma-20: #626c78; -} +/* Runtime theme rules. Canonical light and dark variables live in shared/tokens.css. */ /* Theme changes are captured as one page-level transition. Descendant effects are suppressed briefly so tables and cards cannot repaint on separate timelines. */ @@ -43,142 +25,6 @@ from { opacity: 0; } } -:root[data-theme="dark"] { - color-scheme: dark; - --canvas: #121416; - --surface: #1b1e21; - --surface-muted: #202428; - --surface-subtle: #24282d; - --border: #343a40; - --border-strong: #474f57; - --text-primary: #e8eaed; - --text-secondary: #adb5bd; - --text-tertiary: #7f8993; - --action: #6ca8e8; - --action-hover: #8bbcf0; - --action-soft: #23364a; - --market-up: #f06d73; - --market-up-soft: #40262a; - --market-down: #43bc8a; - --market-down-soft: #1d382f; - --warning-color: #e2ad58; - --warning-soft: #3d3220; - /* Bridge every historical design-token layer to the same dark palette. */ - --xb-gray-25: var(--surface); - --xb-gray-50: var(--surface-muted); - --xb-gray-100: var(--canvas); - --xb-gray-200: var(--border); - --xb-gray-300: var(--border-strong); - --xb-gray-500: var(--text-secondary); - --xb-gray-700: #cbd1d7; - --xb-gray-900: var(--text-primary); - --xb-blue-50: var(--action-soft); - --xb-blue-100: #294866; - --xb-blue-500: var(--action); - --xb-blue-600: var(--action-hover); - --xb-red-50: var(--market-up-soft); - --xb-red-500: var(--market-up); - --xb-green-50: var(--market-down-soft); - --xb-green-500: var(--market-down); - --xb-amber-50: var(--warning-soft); - --xb-amber-500: var(--warning-color); - --surface-canvas: var(--canvas); - --surface-raised: var(--surface); - --surface-selected: var(--action-soft); - --card-bg: var(--surface); - --card-border: var(--border); - --primary: var(--action); - --primary-hover: var(--action-hover); - --danger: var(--market-up); - --success: var(--market-down); - --warning: var(--warning-color); - --r2-blue: var(--action); - --r2-blue-dark: var(--action-hover); - --r2-blue-soft: var(--action-soft); - --r2-blue-line: #42698e; - --r2-up: var(--market-up); - --r2-up-soft: var(--market-up-soft); - --r2-down: var(--market-down); - --r2-down-soft: var(--market-down-soft); - --r2-amber: var(--warning-color); - --r2-amber-soft: var(--warning-soft); - --r2-ink: var(--text-primary); - --r2-sub: var(--text-secondary); - --r2-faint: var(--text-tertiary); - --r2-line: var(--border); - --r2-line-soft: #2a2f34; - --r2-bg: var(--canvas); - --r2-card: var(--surface); - --r2-shadow: var(--shadow-soft); - --bg: var(--canvas); - --card: var(--surface); - --ink: var(--text-primary); - --sub: var(--text-secondary); - --faint: var(--text-tertiary); - --line: var(--border); - --line-soft: #2a2f34; - --line-strong: var(--border-strong); - --text: var(--text-primary); - --text-muted: var(--text-secondary); - --blue: var(--action); - --blue-d: var(--action-hover); - --blue-dark: var(--action-hover); - --blue-soft: var(--action-soft); - --blue-line: #42698e; - --up: var(--market-up); - --up-soft: var(--market-up-soft); - --down: var(--market-down); - --down-soft: var(--market-down-soft); - --coral: var(--market-up); - --coral-soft: var(--market-up-soft); - --green: var(--market-down); - --green-soft: var(--market-down-soft); - --amber: var(--warning-color); - --amber-soft: var(--warning-soft); - --dialog-ink: var(--text-primary); - --dialog-line: var(--border); - --heaven-paper: #191a18; - --heaven-paper-soft: #20211e; - --heaven-ink: #e5e0d4; - --heaven-muted: #aaa497; - --heaven-rule: #3d3a34; - --chart-background: #181b1e; - --chart-grid: #30363c; - --chart-axis: #a3adb6; - --chart-zero: #68737d; - --chart-line: #6ca8e8; - --chart-average: #e2ad58; - --chart-up: #f06d73; - --chart-down: #43bc8a; - --chart-up-volume: rgba(240, 109, 115, .52); - --chart-down-volume: rgba(67, 188, 138, .52); - --chart-area: rgba(108, 168, 232, .12); - --chart-alert-area: rgba(240, 109, 115, .09); - --chart-moving-average: #69737d; - --chart-repair: #e2ad58; - --chart-ma-10: #d39a45; - --chart-ma-20: #9aa5af; - --on-action: #101418; - --warning-line: #6d5a38; - --warning-line-strong: #66502d; - --control-shadow: 0 1px 3px rgba(0, 0, 0, .3); - --dialog-backdrop: rgba(0, 0, 0, .62); - --ladder-level-1: #2d2426; - --ladder-level-2: #2b2822; - --ladder-level-3: #252a2d; - --ladder-level-4: #20282b; - --ladder-level-5: #202428; - --heat-strong-bg: #304f7a; - --heat-strong-ink: #f2f6fb; - --heat-warm-bg: #2b405f; - --heat-warm-ink: #dfeaf7; - --heat-mild-bg: #293440; - --heat-mild-ink: #c7d2dc; - --heaven-field-bg: #23241f; - --shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16); - --shadow: 0 18px 50px rgba(0, 0, 0, .46); -} - :root[data-theme="dark"] :is(html, body, .main, .app-main) { background: var(--canvas); color: var(--text-primary); diff --git a/tests/test_css_governance.py b/tests/test_css_governance.py new file mode 100644 index 0000000..c730ebd --- /dev/null +++ b/tests/test_css_governance.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import re +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +STATIC = ROOT / "static" +TOKENS = STATIC / "shared" / "tokens.css" +LEGACY_STYLESHEETS = ( + "styles.css", + "renovation.css", + "redesign-v2.css", + "design-system.css", + "theme.css", +) + + +class CssGovernanceTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.html = (STATIC / "index.html").read_text(encoding="utf-8") + cls.tokens = TOKENS.read_text(encoding="utf-8") + + def test_token_layer_loads_before_application_styles(self) -> None: + expected_order = ( + "/shared/tokens.css", + "/styles.css", + "/renovation.css", + "/redesign-v2.css", + "/design-system.css", + "/theme.css", + "/wentian-v2.css", + ) + positions = [self.html.index(path) for path in expected_order] + self.assertEqual(positions, sorted(positions)) + + def test_token_file_has_three_layer_contract(self) -> None: + for heading in ( + "/* Primitive tokens */", + "/* Semantic tokens */", + "/* Component tokens */", + "/* Compatibility aliases.", + ): + self.assertIn(heading, self.tokens) + for variable in ( + "--color-action:", + "--surface-canvas:", + "--text-primary:", + "--card-bg:", + "--control-height:", + "--sidebar-width:", + ): + self.assertIn(variable, self.tokens) + + def test_light_and_dark_semantics_share_one_owner(self) -> None: + self.assertIn(':root[data-theme="dark"] {', self.tokens) + global_root = re.compile(r'(?m)^:root(?:\[data-theme="dark"\])?\s*\{') + for filename in LEGACY_STYLESHEETS: + stylesheet = (STATIC / filename).read_text(encoding="utf-8") + self.assertIsNone(global_root.search(stylesheet), filename) + + def test_wentian_tokens_remain_isolated(self) -> None: + self.assertNotRegex(self.tokens, r"--wt-[a-z0-9-]+\s*:") + wentian = (STATIC / "wentian-v2.css").read_text(encoding="utf-8") + self.assertRegex(wentian, r"--wt-[a-z0-9-]+\s*:") + + def test_compatibility_aliases_cover_historical_layers(self) -> None: + for variable in ( + "--blue:", + "--up:", + "--xb-blue-500:", + "--r2-blue:", + "--chart-background:", + "--dragon-profile-list-width:", + ): + self.assertIn(variable, self.tokens) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_frontend_contract.py b/tests/test_frontend_contract.py index 605507f..0cdf9fa 100644 --- a/tests/test_frontend_contract.py +++ b/tests/test_frontend_contract.py @@ -27,6 +27,7 @@ class FrontendContractTests(unittest.TestCase): cls.ui_core = (STATIC_DIR / "ui-core.js").read_text(encoding="utf-8") cls.design_system = (STATIC_DIR / "design-system.css").read_text(encoding="utf-8") cls.theme = (STATIC_DIR / "theme.css").read_text(encoding="utf-8") + cls.tokens = (STATIC_DIR / "shared" / "tokens.css").read_text(encoding="utf-8") collector = IdCollector() collector.feed(cls.html) cls.ids = collector.ids @@ -268,7 +269,7 @@ class FrontendContractTests(unittest.TestCase): self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.theme) self.assertIn("margin-bottom: var(--card-gap);", self.theme) self.assertIn("padding-bottom: var(--card-gap);", self.theme) - self.assertIn("--sentiment-history-max-height:510px;", self.design_system) + self.assertIn("--sentiment-history-max-height: 510px;", self.tokens) self.assertIn("max-height:var(--sentiment-history-max-height);", self.design_system) self.assertIn("overflow:auto;", self.design_system)