refactor: govern CSS tokens and cascade

This commit is contained in:
leefer
2026-07-29 21:56:39 +08:00
parent 8d617cfa17
commit ed2b47fe90
11 changed files with 551 additions and 425 deletions
+14 -13
View File
@@ -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
}
]
}
+49
View File
@@ -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.
-43
View File
@@ -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{
+1
View File
@@ -17,6 +17,7 @@
document.documentElement.style.colorScheme = theme;
})();
</script>
<link rel="stylesheet" href="/shared/tokens.css?v=20260729-1">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/renovation.css?v=20260725-5">
<link rel="stylesheet" href="/redesign-v2.css?v=20260728-1">
-46
View File
@@ -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%;
-83
View File
@@ -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;
+402
View File
@@ -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);
}
-84
View File
@@ -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);
+1 -155
View File
@@ -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);
+82
View File
@@ -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()
+2 -1
View File
@@ -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)