summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 12:03:27 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 12:03:27 +0800
commit1f16a357213611a6d7f84e506064fc0d9c6317c6 (patch)
tree780f53b2068c1c47e2cee9f08075a31514c66d2c /frontend/src
parenta9e5053e51501346bd956609157aa2d1df75f566 (diff)
downloadstrategy-lab-1f16a357213611a6d7f84e506064fc0d9c6317c6.tar.gz
[变更性质] feat(frontend)
[新增功能] round3 主题基建:shadcn preset CLI init(mira)+ app.css 重写 + theme.ts 双主题 [实现方案] 无 legacy 组件选择器的全新 app.css(mint/amber 双 token 组、--radius 0.75rem、保留布局原语);theme.ts 持久化 .theme-amber;utils.ts 本地 cn 实现 [影响范围] frontend/src/app.css、src/lib/theme.ts、utils.ts、package.json
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/app.css411
-rw-r--r--frontend/src/lib/theme.ts28
-rw-r--r--frontend/src/lib/utils.ts6
3 files changed, 184 insertions, 261 deletions
diff --git a/frontend/src/app.css b/frontend/src/app.css
index 11a8e4b..ae195ff 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -1,78 +1,138 @@
@import 'tailwindcss';
@import 'tw-animate-css';
-@import "shadcn-svelte/tailwind.css";
-@import "@fontsource-variable/inter";
+@import 'shadcn-svelte/tailwind.css';
+@import '@fontsource-variable/inter';
@custom-variant dark (&:is(.dark *));
-:root {
- --bg: #f7fafb;
- --surface: #ffffff;
- --surface-2: #f1f5f6;
- --border: #e2e8ea;
+/* ==== round3 主题 sessions tokens ====
+ 来源:用户在 shadcn 官网 create 的 Stripe 风格参考截图(preset b1D0dv72 CLI
+ 拉取成功但落的是 neutral 默认,故按截图色值重配 tokens)。
+ 参考取样:mint 主色 #70DAB6 / 深一档 #58D3AA;amber 主色 #F0AE00。
+ 卡片白底、radius 0.75rem、大字重数字 tabular-nums。默认 mint,.theme-amber 切换。 */
+
+:root,
+.dark {
+ /* shadcn tokens — white card, Stripe-ish neutrals */
+ --radius: 0.75rem;
+ --background: #f7fafb;
+ --foreground: #1e2a30;
+ --card: #ffffff;
+ --card-foreground: #1e2a30;
+ --popover: #ffffff;
+ --popover-foreground: #1e2a30;
+ --primary: #0e3a30;
+ --primary-foreground: #ffffff;
+ --secondary: #eef3f4;
+ --secondary-foreground: #26414a;
+ --muted: #eef3f4;
+ --muted-foreground: #5c6c74;
+ --accent: #e3f7ef;
+ --accent-foreground: #0b5d47;
+ --destructive: #b91c1c;
+ --input: #d6e0e3;
+ --ring: #70dab6;
+ --border: #e2eaec;
+ /* mint demo(默认) */
+ --brand: #58d3aa;
+ --brand-strong: #0f9f7a;
+ --brand-soft: #e3f7ef;
+ --brand-ink: #0b4a3a;
+ /* chart 家族(mint 梯度,贴近截图的柔和强调) */
+ --chart-1: #58d3aa;
+ --chart-2: #0f9f7a;
+ --chart-3: #0b4a3a;
+ --chart-4: #c4b37a;
+ --chart-5: #94a6ac;
+ --sidebar: #ffffff;
+ --sidebar-foreground: #1e2a30;
+ --sidebar-border: #e2eaec;
+ /* legacy alias(迁移期语义映射,等价 token 而非旧组件样式) */
+ --bg: var(--background);
+ --surface: var(--card);
+ --surface-2: var(--secondary);
--border-strong: #cbd5d8;
- --text: #1e2a30;
- --text-2: #4b5a62;
+ --text: var(--foreground);
+ --text-2: var(--muted-foreground);
--text-3: #7b8a92;
- --teal: #0d9488;
- --teal-strong: #0f766e;
- --teal-soft: #e6f4f2;
- --ink: #12312d;
- --danger: #b91c1c;
+ --teal: var(--brand);
+ --teal-strong: var(--brand-strong);
+ --teal-soft: var(--brand-soft);
+ --ink: var(--brand-ink);
--danger-soft: #fdf2f2;
--warn: #92400e;
--warn-soft: #fff9ec;
--ok: #15803d;
--ok-soft: #ecfdf3;
- --radius: 0.375rem;
--shadow: 0 1px 2px rgba(16, 42, 49, 0.05), 0 2px 8px rgba(16, 42, 49, 0.05);
--mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, 'Noto Sans Mono CJK SC', monospace;
--font: -apple-system, 'SF Pro Text', BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
'Noto Sans CJK SC', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- --background: #f7fafb;
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: #0f766e;
+ --sidebar-primary: var(--primary);
+ --sidebar-primary-foreground: var(--primary-foreground);
+ --sidebar-accent: var(--secondary);
+ --sidebar-accent-foreground: var(--foreground);
+ --sidebar-ring: var(--ring);
+}
+
+/* amber demo:第二套 CSS 变量组(src/lib/theme.ts 切换 .theme-amber) */
+.theme-amber {
+ --primary: #b87700;
--primary-foreground: #ffffff;
- --secondary: #f1f5f6;
- --secondary-foreground: oklch(0.205 0 0);
- --muted: #f1f5f6;
- --muted-foreground: oklch(0.556 0 0);
- --accent: #f1f5f6;
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --input: #cbd5d8;
- --ring: #0d9488;
- --chart-1: #0f766e;
- --chart-2: #b45309;
- --chart-3: #1d4ed8;
- --chart-4: #8b1d8b;
- --chart-5: #64748b;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
+ --accent: #fdf3d7;
+ --accent-foreground: #7a5200;
+ --ring: #f0ae00;
+ --brand: #f0ae00;
+ --brand-strong: #c08700;
+ --brand-soft: #fdf3d7;
+ --brand-ink: #5d3f00;
+ --chart-1: #f0ae00;
+ --chart-2: #b87700;
+ --chart-3: #6e4a00;
+ --chart-4: #58d3aa;
+ --chart-5: #94a6ac;
+}
+
+/* legacy 名义色到主题的映射:amber 组同样生效 */
+.theme-amber { --teal: var(--brand); --teal-strong: var(--brand-strong); --teal-soft: var(--brand-soft); --ink: var(--brand-ink); }
+
+.dark {
+ --background: #10181b;
+ --foreground: #eef3f4;
+ --card: #16211f;
+ --card-foreground: #eef3f4;
+ --popover: #16201f;
+ --popover-foreground: #eef3f4;
+ --primary: #58d3aa;
+ --primary-foreground: #0b1a16;
+ --secondary: #1c2a27;
+ --secondary-foreground: #eef3f4;
+ --muted: #1c2a27;
+ --muted-foreground: #93a5ab;
+ --accent: #12332c;
+ --accent-foreground: #8aeccf;
+ --destructive: #e05252;
+ --input: #243437;
+ --ring: #58d3aa;
+ --border: #223433;
+ --border-strong: #2c4340;
+ --brand-soft: rgba(88, 211, 170, 0.14);
+ --text-2: #93a5ab;
+ --text-3: #7b8a92;
+ --danger-soft: rgba(185, 28, 28, 0.2);
+ --warn-soft: rgba(146, 64, 14, 0.2);
+ --ok: #4ade80;
}
* { box-sizing: border-box; }
-/* round2 质感:150ms 全局过渡(背景/边框/阴影/位移/文字色) */
-a, button, input, select, textarea, table.data th, table.data td,
-.card, [data-slot='card'], .badge, .navlink, .metric-card {
+/* 全局 150ms 微过渡 */
+a, button, input, select, textarea, [data-slot='card'], .navlink {
transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease,
box-shadow 150ms ease, transform 150ms ease;
}
-/* 桌面端卡片 hover 轻微上浮阴影;触屏(hover none)不做位移避免黏连 */
@media (hover: hover) and (min-width: 768px) {
- .card:hover,
[data-slot='card']:hover {
box-shadow: 0 4px 14px rgba(16, 42, 49, 0.08);
transform: translateY(-1px);
@@ -82,189 +142,63 @@ a, button, input, select, textarea, table.data th, table.data td,
html, body {
margin: 0;
padding: 0;
- background: var(--bg);
- color: var(--text);
+ background: var(--background);
+ color: var(--foreground);
font-family: var(--font);
font-size: 14.5px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
+ background-color: var(--background);
}
h1, h2, h3, h4 { font-weight: 600; margin: 0; }
-/* round2 字阶:拉开 h1/h2/正文/次要说明的区分 */
h1 { font-size: 1.55rem; letter-spacing: -0.02em; line-height: 1.25; }
h2 { font-size: 1.18rem; letter-spacing: -0.01em; }
h3 { font-size: 1.02rem; }
-.hint { color: var(--text-3); font-size: 0.82rem; line-height: 1.45; }
-a { color: var(--teal-strong); text-decoration: none; }
+
+a { color: var(--brand-strong); text-decoration: none; }
a:hover { text-decoration: underline; }
p { margin: 0.4rem 0; }
button, input, select, textarea { font: inherit; color: inherit; }
+button { cursor: pointer; }
-.btn {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
- padding: 0.45rem 0.95rem;
- border-radius: var(--radius);
- border: 1px solid var(--border-strong);
- background: var(--surface);
- color: var(--text);
- cursor: pointer;
- font-size: 0.92rem;
- transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
- white-space: nowrap;
-}
-.btn:hover:not(:disabled) { background: var(--surface-2); }
-.btn:disabled { opacity: 0.5; cursor: not-allowed; }
-.btn:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, a:focus-visible {
- outline: 2px solid var(--teal);
- outline-offset: 1px;
-}
-.btn.primary {
- background: var(--teal-strong);
- border-color: var(--teal-strong);
- color: #fff;
-}
-.btn.primary:hover:not(:disabled) { background: #115e59; }
-.btn.danger { color: var(--danger); border-color: #f2c7c7; }
-.btn.danger:hover:not(:disabled) { background: var(--danger-soft); }
-.btn.ghost { border-color: transparent; background: transparent; }
-.btn.ghost:hover:not(:disabled) { background: var(--surface-2); }
-.btn.small { padding: 0.25rem 0.6rem; font-size: 0.83rem; }
-.btn[aria-pressed='true'] { background: var(--teal-soft); border-color: var(--teal); color: var(--ink); }
-
-label { display: block; font-size: 0.84rem; color: var(--text-2); font-weight: 500; margin-bottom: 0.25rem; }
-input, select, textarea.plain {
- width: 100%;
- padding: 0.45rem 0.65rem;
- border: 1px solid var(--border-strong);
- border-radius: 8px;
- background: var(--surface);
- font-size: 0.92rem;
-}
-input[type='checkbox'], input[type='radio'] { width: auto; }
-
-.card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 1rem 1.15rem;
- box-shadow: var(--shadow);
-}
-.card + .card { margin-top: 0.9rem; }
-
-table.data {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.88rem;
-}
-table.data th {
- text-align: left;
- font-weight: 600;
- color: var(--text-2);
- border-bottom: 1px solid var(--border-strong);
- padding: 0.45rem 0.6rem;
- white-space: nowrap;
-}
-table.data td {
- border-bottom: 1px solid var(--border);
- padding: 0.42rem 0.6rem;
- vertical-align: top;
-}
-table.data tr:hover td { background: var(--surface-2); }
-.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
-
-.badge {
- display: inline-block;
- padding: 0.1rem 0.5rem;
- border-radius: 999px;
- font-size: 0.76rem;
- font-weight: 600;
- border: 1px solid transparent;
-}
-.badge.teal { background: var(--teal-soft); color: var(--teal-strong); }
-.badge.grey { background: var(--surface-2); color: var(--text-2); border-color: var(--border); }
-.badge.warn { background: var(--warn-soft); color: var(--warn); border-color: #f0ddb4; }
-.badge.danger { background: var(--danger-soft); color: var(--danger); border-color: #f2c7c7; }
-.badge.ok { background: var(--ok-soft); color: var(--ok); border-color: #c3ecd0; }
-
-.banner {
- border-radius: var(--radius);
- padding: 0.65rem 0.9rem;
- font-size: 0.9rem;
- border: 1px solid;
- display: flex;
- gap: 0.6rem;
- align-items: flex-start;
-}
-.banner.error { background: var(--danger-soft); border-color: #f2c7c7; color: var(--danger); }
-.banner.warn { background: var(--warn-soft); border-color: #f0ddb4; color: var(--warn); }
-.banner.info { background: var(--teal-soft); border-color: #c8eae6; color: var(--teal-strong); }
-
+/* 布局原语(非组件 facade):排列结构工具 */
.stack { display: flex; flex-direction: column; gap: 0.75rem; }
.row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.spread { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
+.cols-2 { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1rem; align-items: start; }
+.table-wrap { overflow-x: auto; min-width: 0; }
+.card, .stack, .row, .spread { min-width: 0; }
-.tabs {
- display: flex;
- gap: 0.25rem;
- border-bottom: 1px solid var(--border);
-}
-.tabs button {
- padding: 0.55rem 1rem;
- border: none;
- background: none;
- color: var(--text-2);
- font-size: 0.95rem;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- border-radius: 6px 6px 0 0;
-}
-.tabs button:hover { background: var(--surface-2); }
-.tabs button[aria-selected='true'] {
- color: var(--teal-strong);
- font-weight: 600;
- border-bottom-color: var(--teal);
-}
-
-/* metric tiles shared by the results card and other metric rows; without
- these the six result metrics stacked vertically with large blank space */
-.metric-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 0.7rem;
-}
-.metric {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 0.6rem 0.85rem;
- min-width: 0;
-}
+/* Semaphore 数字排印(Stripe 仪表感):大字重 + 等宽数字 */
+.num { font-variant-numeric: tabular-nums; font-family: var(--mono); }
.mlabel { color: var(--text-3); font-size: 0.8rem; }
.mvalue {
- font-size: 1.15rem;
+ font-size: 1.5rem;
font-weight: 700;
- color: var(--text);
+ letter-spacing: -0.01em;
+ color: var(--foreground);
font-variant-numeric: tabular-nums;
+ font-family: var(--mono);
overflow-wrap: anywhere;
}
-.cols-2 { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1rem; align-items: start; }
-.table-wrap { overflow-x: auto; min-width: 0; }
-
-@media (max-width: 900px) {
- .cols-2 { grid-template-columns: minmax(0, 1fr); }
- .metric-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
+.mvalue-sub { color: var(--text-3); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
+.metric-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 0.7rem;
}
+.run-split { display: inline-flex; gap: 0.5rem; align-items: baseline; }
+.run-split i { font-style: normal; color: var(--text-3); font-size: 0.75rem; }
-code, pre {
- font-family: var(--mono);
- font-size: 0.85rem;
-}
+.hint { color: var(--text-3); font-size: 0.82rem; line-height: 1.45; }
+label { display: block; font-size: 0.84rem; color: var(--text-2); font-weight: 500; margin-bottom: 0.25rem; }
+input, select, textarea { font: inherit; color: inherit; }
+
+code, pre { font-family: var(--mono); font-size: 0.85rem; }
pre { margin: 0; white-space: pre-wrap; word-break: break-all; }
.sr-only {
@@ -275,63 +209,12 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-all; }
border: 0;
}
-/* ---- narrow-viewport wrapping (no global content hiding) ----
- Real provider error strings contain long unbreakable URLs; .spread/.stack
- children would otherwise propagate min-content outwards and inflate the
- document width on mobile. `overflow-wrap` keeps every word reachable while
- wrapping; nothing is hidden — long tables get a local scroll container. */
-.card, .stack, .row, .spread, .banner { min-width: 0; }
+/* narrow-viewport wrapping(保留:文档级防溢出行为,非组件样式) */
.banner { overflow-wrap: anywhere; }
.banner .err-list { overflow-wrap: anywhere; }
-.card table.data { min-width: 0; }
-
-@media (max-width: 900px) {
- /* collapse multi-column forms: the dataset wizard grid3 + label min-widths
- measured 523px min-content at 375px viewport (mobile-overflow-probe) */
- .grid2, .grid3 { grid-template-columns: minmax(0, 1fr); }
- /* wrap table cells instead of nowrap inflating min-content; a truly wide
- tabular block still gets its own local scrollbar (.scrollx), content
- remains fully accessible without a document-level horizontal overflow */
- table.data th, table.data td {
- white-space: normal;
- overflow-wrap: anywhere;
- }
- .btn { white-space: normal; }
-}
-
-.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: #2dd4bf;
- --primary-foreground: #04211f;
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: #0f766e;
- --chart-2: #b45309;
- --chart-3: #1d4ed8;
- --chart-4: #8b1d8b;
- --chart-5: #64748b;
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
-}
+.table.data, table.data { min-width: 0; }
+.mono-invite { font-family: var(--mono); font-size: 0.8rem; word-break: break-all; }
+.ul-plain { margin: 0.15rem 0 0; padding-left: 1.1rem; }
@theme inline {
--font-sans: 'Inter Variable', sans-serif;
@@ -373,6 +256,7 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-all; }
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
+ --radius-0.75rem: var(--radius);
}
@layer base {
@@ -386,3 +270,14 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-all; }
@apply font-sans;
}
}
+
+/* ---- 窄视口收敛(保留行为:不做全局隐藏,仅塌缩/换行 + 局部滚动) ---- */
+@media (max-width: 900px) {
+ .cols-2 { grid-template-columns: minmax(0, 1fr); }
+ .metric-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
+ .grid2, .grid3 { grid-template-columns: minmax(0, 1fr); }
+ table.data th, table.data td {
+ white-space: normal;
+ overflow-wrap: anywhere;
+ }
+}
diff --git a/frontend/src/lib/theme.ts b/frontend/src/lib/theme.ts
new file mode 100644
index 0000000..ad949a4
--- /dev/null
+++ b/frontend/src/lib/theme.ts
@@ -0,0 +1,28 @@
+// 主题切换(round3):默认 mint;.theme-amber 类挂 <html> 切到 amber 变量组。
+// 两套 token 都是 app.css 里的 CSS 变量组,本模块只负责持久化与挂类。
+
+export type BrandTheme = 'mint' | 'amber';
+const KEY = 'sl-brand-theme';
+
+export function currentTheme(): BrandTheme {
+ if (typeof document === 'undefined' || typeof localStorage === 'undefined') return 'mint';
+ const stored = localStorage.getItem(KEY);
+ return stored === 'amber' ? 'amber' : 'mint';
+}
+
+export function applyTheme(t: BrandTheme): void {
+ if (typeof document === 'undefined') return;
+ if (t === 'amber') document.documentElement.classList.add('theme-amber');
+ else document.documentElement.classList.remove('theme-amber');
+ if (typeof localStorage !== 'undefined') localStorage.setItem(KEY, t);
+}
+
+export function toggleTheme(): BrandTheme {
+ const next: BrandTheme = currentTheme() === 'mint' ? 'amber' : 'mint';
+ applyTheme(next);
+ return next;
+}
+
+if (typeof document !== 'undefined') {
+ applyTheme(currentTheme());
+}
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
index 55b3a91..8048253 100644
--- a/frontend/src/lib/utils.ts
+++ b/frontend/src/lib/utils.ts
@@ -1,7 +1,7 @@
-import { clsx, type ClassValue } from "clsx";
-import { twMerge } from "tailwind-merge";
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
-export function cn(...inputs: ClassValue[]) {
+export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}