@zmzai/theme 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmzai/theme",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "zmzai 全品牌设计系统 — Radix UI + framer-motion + Tailwind v4 + MiSans",
@@ -60,12 +60,6 @@
60
60
  "README.md",
61
61
  "LICENSE"
62
62
  ],
63
- "scripts": {
64
- "build": "tsc -p tsconfig.json --noEmit",
65
- "typecheck": "tsc --noEmit",
66
- "playground": "vite playground --port 5174",
67
- "prepublishOnly": "pnpm typecheck"
68
- },
69
63
  "peerDependencies": {
70
64
  "react": "^19.0.0",
71
65
  "react-dom": "^19.0.0",
@@ -105,5 +99,9 @@
105
99
  "remark-gfm": "^4.0.1",
106
100
  "tailwind-merge": "^2.5.0"
107
101
  },
108
- "packageManager": "pnpm@10.34.5+sha512.a4ee05f2f73658255bd6a89859c065a45c28a57daefae2c893a168ee2b73168c37b91e83e57ea67654ad03f03031746430e8bce38e362e042605fb8abc80192e"
109
- }
102
+ "scripts": {
103
+ "build": "tsc -p tsconfig.json --noEmit",
104
+ "typecheck": "tsc --noEmit",
105
+ "playground": "vite playground --port 5174"
106
+ }
107
+ }
Binary file
@@ -0,0 +1,211 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import type { ComponentType, ReactNode } from "react";
5
+
6
+ import { Badge } from "../badge/Badge";
7
+ import { Icon, type IconName } from "../icon/Icon";
8
+ import { CommandPalette, type AppPaletteItem } from "./CommandPalette";
9
+
10
+ export type AppNavItem = { label: string; href: string; icon: IconName; keywords?: string };
11
+ export type AppNavSection = { label: string; items: AppNavItem[] };
12
+ export type { AppPaletteItem } from "./CommandPalette";
13
+
14
+ /** 消费端注入的链接组件(Next.js 传 next/link 以保持 SPA 导航;缺省用 <a>)。 */
15
+ export type AppShellLink = ComponentType<{
16
+ href: string;
17
+ className?: string;
18
+ children?: ReactNode;
19
+ onClick?: () => void;
20
+ "aria-current"?: "page" | undefined;
21
+ }>;
22
+
23
+ export function appShellIsActive(pathname: string, href: string): boolean {
24
+ return pathname === href || pathname.startsWith(`${href}/`);
25
+ }
26
+
27
+ /** 无 SPA 需求时的缺省链接:普通 <a>。 */
28
+ function DefaultAnchor({ href, className, children, onClick, "aria-current": ariaCurrent }: { href: string; className?: string; children?: ReactNode; onClick?: () => void; "aria-current"?: "page" }) {
29
+ return (
30
+ <a href={href} className={className} onClick={onClick} aria-current={ariaCurrent}>
31
+ {children}
32
+ </a>
33
+ );
34
+ }
35
+
36
+ function SidebarSections({
37
+ sections,
38
+ pathname,
39
+ link: Link,
40
+ onNavigate,
41
+ }: {
42
+ sections: AppNavSection[];
43
+ pathname: string;
44
+ link: AppShellLink;
45
+ onNavigate?: () => void;
46
+ }) {
47
+ return (
48
+ <>
49
+ {sections.map((section) => (
50
+ <div key={section.label} className="flex flex-col gap-0.5">
51
+ <p className="px-2 pb-1 font-mono text-[10px] uppercase tracking-wider text-ink-3">{section.label}</p>
52
+ {section.items.map((item) => {
53
+ const active = appShellIsActive(pathname, item.href);
54
+ return (
55
+ <Link
56
+ key={item.href}
57
+ href={item.href}
58
+ onClick={onNavigate}
59
+ aria-current={active ? "page" : undefined}
60
+ className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px] transition-colors ${
61
+ active ? "bg-surface font-medium text-accent-readable" : "text-muted hover:bg-surface hover:text-accent"
62
+ }`}
63
+ >
64
+ <Icon name={item.icon} size={13} />
65
+ {item.label}
66
+ </Link>
67
+ );
68
+ })}
69
+ </div>
70
+ ))}
71
+ </>
72
+ );
73
+ }
74
+
75
+ /**
76
+ * 全站统一应用外壳(OpenRouter 式交互内核):
77
+ * 全高左侧 sidebar(分组导航)+ 轻顶栏(标题 + ⌘K)+ 侧栏底部账户块。
78
+ * 站点无关:导航分组、品牌区、账户块、链接组件均由调用方注入;
79
+ * relay 的 RelayShell / sandbox 的 SandboxShell 是它的薄封装。
80
+ */
81
+ export function AppShell({
82
+ brand,
83
+ sections,
84
+ pathname,
85
+ link,
86
+ account = null,
87
+ headerExtras = null,
88
+ paletteItems,
89
+ children,
90
+ }: {
91
+ /** 品牌区:侧栏顶部与顶栏兜底标题 */
92
+ brand: { label: string; suffix?: string; href?: string };
93
+ /** 侧栏分组导航 */
94
+ sections: AppNavSection[];
95
+ /** 当前路径(消费端传 usePathname() ?? "/";theme 不依赖 next) */
96
+ pathname: string;
97
+ /** 链接组件(Next.js 站传 next/link) */
98
+ link?: AppShellLink;
99
+ /** 侧栏底部账户块(登录按钮 / 用户信息) */
100
+ account?: ReactNode;
101
+ /** 顶栏右侧扩展(余额、状态等) */
102
+ headerExtras?: ReactNode;
103
+ /** ⌘K 命令面板条目;为空时顶栏不显示搜索按钮 */
104
+ paletteItems?: AppPaletteItem[];
105
+ children: ReactNode;
106
+ }) {
107
+ const Link = (link ?? DefaultAnchor) as AppShellLink;
108
+ const hasPalette = Boolean(paletteItems?.length);
109
+ const [drawerOpen, setDrawerOpen] = useState(false);
110
+ const [paletteOpen, setPaletteOpen] = useState(false);
111
+
112
+ // 顶栏标题:当前激活项的 label(取最长匹配,/admin/models 优先于 /admin)
113
+ const currentLabel = useMemo(() => {
114
+ const matches = sections.flatMap((s) => s.items).filter((item) => appShellIsActive(pathname, item.href));
115
+ if (!matches.length) return brand.label;
116
+ return matches.sort((a, b) => b.href.length - a.href.length)[0].label;
117
+ }, [pathname, sections, brand.label]);
118
+
119
+ // ⌘K / Ctrl+K 全局快捷键
120
+ useEffect(() => {
121
+ if (!hasPalette) return;
122
+ const onKey = (event: KeyboardEvent) => {
123
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
124
+ event.preventDefault();
125
+ setPaletteOpen((open) => !open);
126
+ }
127
+ };
128
+ window.addEventListener("keydown", onKey);
129
+ return () => window.removeEventListener("keydown", onKey);
130
+ }, [hasPalette]);
131
+
132
+ const brandBlock = (
133
+ <Link href={brand.href ?? "/"} className="flex items-baseline gap-1.5 px-2">
134
+ <span className="text-sm font-semibold tracking-tight">{brand.label}</span>
135
+ {brand.suffix ? <span className="font-mono text-[11px] text-ink-3">{brand.suffix}</span> : null}
136
+ </Link>
137
+ );
138
+
139
+ return (
140
+ <div className="min-h-dvh bg-bg">
141
+ {hasPalette ? <CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} items={paletteItems!} /> : null}
142
+
143
+ {/* 移动端抽屉 */}
144
+ {drawerOpen ? (
145
+ <div className="fixed inset-0 z-40 lg:hidden" role="presentation">
146
+ <div className="absolute inset-0 bg-black/30" onClick={() => setDrawerOpen(false)} />
147
+ <nav className="absolute inset-y-0 left-0 flex w-64 flex-col gap-5 overflow-y-auto border-r border-line bg-bg px-3 py-4">
148
+ {brandBlock}
149
+ <SidebarSections sections={sections} pathname={pathname} link={Link} onNavigate={() => setDrawerOpen(false)} />
150
+ {account}
151
+ </nav>
152
+ </div>
153
+ ) : null}
154
+
155
+ <div className="lg:grid lg:grid-cols-[13.5rem_minmax(0,1fr)]">
156
+ {/* 桌面固定侧栏 */}
157
+ <aside className="sticky top-0 hidden h-dvh flex-col gap-5 overflow-y-auto border-r border-line px-3 py-4 lg:flex">
158
+ {brandBlock}
159
+ <SidebarSections sections={sections} pathname={pathname} link={Link} />
160
+ {account}
161
+ </aside>
162
+
163
+ <div className="flex min-h-dvh min-w-0 flex-col">
164
+ {/* 轻顶栏 */}
165
+ <header className="sticky top-0 z-30 flex items-center gap-3 border-b border-line bg-bg/95 px-4 py-2.5 backdrop-blur lg:px-8">
166
+ <button
167
+ type="button"
168
+ aria-label="打开导航"
169
+ onClick={() => setDrawerOpen(true)}
170
+ className="rounded-md p-1 text-muted hover:bg-surface hover:text-accent lg:hidden"
171
+ >
172
+ <Icon name="menu" size={16} />
173
+ </button>
174
+ <span className="text-sm font-medium text-ink-2">{currentLabel}</span>
175
+ <div className="ml-auto flex items-center gap-3">
176
+ {headerExtras}
177
+ {hasPalette ? (
178
+ <button
179
+ type="button"
180
+ onClick={() => setPaletteOpen(true)}
181
+ className="flex items-center gap-2 rounded-md border border-line px-2.5 py-1 font-mono text-[11px] text-muted transition-colors hover:border-accent hover:text-accent"
182
+ aria-label="打开命令面板"
183
+ >
184
+ <Icon name="search" size={12} />
185
+ <span className="hidden sm:inline">搜索</span>
186
+ <kbd className="hidden sm:inline">⌘K</kbd>
187
+ </button>
188
+ ) : null}
189
+ </div>
190
+ </header>
191
+
192
+ {/* 内容区 */}
193
+ <main className="page-shell flex-1 py-8">{children}</main>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ /** 侧栏账户块通用样式辅助:登录后的用户信息行。 */
201
+ export function AppShellAccountRow({ name, badge, children }: { name: string; badge?: string; children?: ReactNode }) {
202
+ return (
203
+ <div className="mt-auto flex flex-col gap-2 border-t border-line px-2 pt-3">
204
+ <div className="flex items-center justify-between gap-2">
205
+ <span className="truncate font-mono text-xs text-ink-2">{name}</span>
206
+ {badge ? <Badge variant="outline" size="sm">{badge}</Badge> : null}
207
+ </div>
208
+ {children ? <div className="flex items-center justify-between gap-2 font-mono text-[11px]">{children}</div> : null}
209
+ </div>
210
+ );
211
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ import { Icon, type IconName } from "../icon/Icon";
6
+
7
+ export type AppPaletteItem = {
8
+ label: string;
9
+ hint?: string;
10
+ group: string;
11
+ icon?: IconName;
12
+ keywords?: string;
13
+ run: () => void;
14
+ };
15
+
16
+ /**
17
+ * ⌘K / Ctrl+K 命令面板:纯手写 overlay(无新依赖)。
18
+ * 键盘:↑↓ 选择、Enter 确认、Esc 关闭;输入即时过滤(label/hint/keywords 子串匹配)。
19
+ */
20
+ export function CommandPalette({ open, onClose, items, placeholder = "搜索页面或执行动作…" }: { open: boolean; onClose: () => void; items: AppPaletteItem[]; placeholder?: string }) {
21
+ const [query, setQuery] = useState("");
22
+ const [cursor, setCursor] = useState(0);
23
+ const inputRef = useRef<HTMLInputElement>(null);
24
+ const listRef = useRef<HTMLDivElement>(null);
25
+
26
+ const filtered = useMemo(() => {
27
+ const q = query.trim().toLowerCase();
28
+ if (!q) return items;
29
+ return items.filter((item) => `${item.label} ${item.hint ?? ""} ${item.keywords ?? ""}`.toLowerCase().includes(q));
30
+ }, [items, query]);
31
+
32
+ useEffect(() => {
33
+ if (open) {
34
+ setQuery("");
35
+ setCursor(0);
36
+ // 等一帧让 overlay 挂载后再聚焦,避免被浏览器焦点还原打断
37
+ requestAnimationFrame(() => inputRef.current?.focus());
38
+ }
39
+ }, [open]);
40
+
41
+ useEffect(() => setCursor(0), [query]);
42
+
43
+ useEffect(() => {
44
+ if (!open) return;
45
+ const onKey = (event: KeyboardEvent) => {
46
+ if (event.key === "Escape") {
47
+ event.preventDefault();
48
+ onClose();
49
+ } else if (event.key === "ArrowDown") {
50
+ event.preventDefault();
51
+ setCursor((c) => Math.min(c + 1, filtered.length - 1));
52
+ } else if (event.key === "ArrowUp") {
53
+ event.preventDefault();
54
+ setCursor((c) => Math.max(c - 1, 0));
55
+ } else if (event.key === "Enter") {
56
+ event.preventDefault();
57
+ const item = filtered[cursor];
58
+ if (item) {
59
+ onClose();
60
+ item.run();
61
+ }
62
+ }
63
+ };
64
+ window.addEventListener("keydown", onKey);
65
+ return () => window.removeEventListener("keydown", onKey);
66
+ }, [open, filtered, cursor, onClose]);
67
+
68
+ useEffect(() => {
69
+ listRef.current?.querySelector<HTMLElement>(`[data-index="${cursor}"]`)?.scrollIntoView({ block: "nearest" });
70
+ }, [cursor]);
71
+
72
+ if (!open) return null;
73
+
74
+ const execute = (index: number) => {
75
+ const item = filtered[index];
76
+ if (!item) return;
77
+ onClose();
78
+ item.run();
79
+ };
80
+
81
+ return (
82
+ <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/30 px-4 pt-[12vh]" onClick={onClose} role="presentation">
83
+ <div
84
+ className="w-full max-w-xl overflow-hidden rounded-xl border border-line bg-bg shadow-lg"
85
+ onClick={(event) => event.stopPropagation()}
86
+ role="dialog"
87
+ aria-modal="true"
88
+ aria-label="命令面板"
89
+ >
90
+ <div className="flex items-center gap-2 border-b border-line px-4 py-3">
91
+ <Icon name="search" size={14} className="text-muted" />
92
+ <input
93
+ ref={inputRef}
94
+ value={query}
95
+ onChange={(event) => setQuery(event.target.value)}
96
+ placeholder={placeholder}
97
+ className="w-full bg-transparent text-sm outline-none placeholder:text-ink-3"
98
+ />
99
+ <kbd className="rounded border border-line px-1.5 py-0.5 font-mono text-[10px] text-ink-3">esc</kbd>
100
+ </div>
101
+ <div ref={listRef} className="max-h-[50vh] overflow-y-auto py-1.5">
102
+ {filtered.length === 0 ? (
103
+ <p className="px-4 py-6 text-center font-mono text-xs text-ink-3">没有匹配结果</p>
104
+ ) : (
105
+ filtered.map((item, index) => (
106
+ <button
107
+ key={`${item.group}-${item.label}`}
108
+ type="button"
109
+ data-index={index}
110
+ onClick={() => execute(index)}
111
+ onMouseEnter={() => setCursor(index)}
112
+ className={`flex w-full items-center gap-2.5 px-4 py-2 text-left text-sm transition-colors ${
113
+ index === cursor ? "bg-surface text-accent-readable" : "text-ink-2"
114
+ }`}
115
+ >
116
+ {item.icon ? <Icon name={item.icon} size={13} className="shrink-0 text-muted" /> : null}
117
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
118
+ {item.hint ? <span className="shrink-0 font-mono text-[11px] text-ink-3">{item.hint}</span> : null}
119
+ </button>
120
+ ))
121
+ )}
122
+ </div>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,4 @@
1
+ export { AppShell, AppShellAccountRow, appShellIsActive } from "./AppShell";
2
+ export type { AppNavItem, AppNavSection, AppShellLink } from "./AppShell";
3
+ export { CommandPalette } from "./CommandPalette";
4
+ export type { AppPaletteItem } from "./CommandPalette";
@@ -1,12 +1,12 @@
1
1
  /* ArtifactCard — 产物卡。token 依赖消费端 @theme。 */
2
2
  .zmz-artifact-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.15rem 0.6rem; align-items: center; text-align: left; border: 1px solid var(--color-line); border-radius: var(--radius); background: var(--color-bg); padding: 0.6rem 0.75rem; cursor: pointer; font: inherit; color: inherit; transition: border-color 0.15s var(--ease-out), box-shadow 0.15s var(--ease-out); }
3
- .zmz-artifact-card:hover { border-color: var(--color-accent); box-shadow: 0 4px 16px color-mix(in oklab, var(--color-accent) 12%, transparent); }
3
+ .zmz-artifact-card:hover { border-color: var(--color-ink-3); box-shadow: var(--shadow-md); }
4
4
  /* 显式 grid 定位:name/meta 固定在左列(1fr),actions 固定右列跨两行。
5
5
  自动放置曾把超长 MIME 文本(无空格、无断行)排进右列 auto 轨道,
6
6
  撑爆布局把名字列压成 0 → 文件名竖排逐字折行。 */
7
7
  .zmz-artifact-name { grid-column: 1; grid-row: 1; font-family: var(--font-mono); font-size: 0.75rem; overflow-wrap: anywhere; }
8
8
  .zmz-artifact-meta { grid-column: 1; grid-row: 2; font-size: 0.6875rem; color: var(--color-muted); overflow-wrap: anywhere; }
9
9
  .zmz-artifact-actions { grid-column: 2; grid-row: 1 / span 2; display: flex; align-items: center; gap: 0.5rem; }
10
- .zmz-artifact-preview-hint { font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-accent-strong); }
10
+ .zmz-artifact-preview-hint { font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-ink-3); }
11
11
  .zmz-artifact-download { display: inline-flex; color: var(--color-muted); }
12
- .zmz-artifact-download:hover { color: var(--color-accent); }
12
+ .zmz-artifact-download:hover { color: var(--color-ink); }
@@ -9,7 +9,7 @@ export const badgeVariants = cva(
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
- solid: "bg-ink text-white",
12
+ solid: "bg-ink text-bg",
13
13
  outline: "border border-line text-ink-2",
14
14
  success: "border border-success/30 bg-success/10 text-success",
15
15
  warning: "border border-warning/30 bg-warning/10 text-warning",
@@ -26,7 +26,8 @@ export const buttonVariants = cva(
26
26
  variants: {
27
27
  variant: {
28
28
  primary:
29
- "bg-ink text-white hover:shadow-md active:scale-[0.98]",
29
+ // 反色按钮:bg-ink 上用 bg 色作文字(浅色=白、深色=黑),硬编码 text-white 在深色主题下不可读
30
+ "bg-ink text-bg hover:shadow-md active:scale-[0.98]",
30
31
  secondary:
31
32
  "border border-line text-ink hover:border-ink hover:bg-surface-2 active:scale-[0.98]",
32
33
  ghost:
@@ -1,6 +1,7 @@
1
1
  // @zmzai/theme — Components barrel (54 components)
2
2
 
3
3
  // === Basic ===
4
+ export * from "./app-shell";
4
5
  export * from "./button";
5
6
  export * from "./input";
6
7
  export * from "./textarea";
@@ -40,8 +40,8 @@
40
40
  }
41
41
 
42
42
  .btn-solid:hover {
43
- background: oklch(0.30 0.010 30);
44
- border-color: oklch(0.30 0.010 30);
43
+ background: var(--color-ink);
44
+ border-color: var(--color-ink);
45
45
  }
46
46
 
47
47
  /* Arrow slide on hover(按钮/链接内的 → 箭头位移) */
@@ -125,7 +125,7 @@
125
125
 
126
126
  .product-card:hover {
127
127
  transform: translateY(-4px);
128
- box-shadow: 0 16px 48px -12px oklch(0.18 0.011 40 / 0.12);
128
+ box-shadow: var(--shadow-lg);
129
129
  border-color: var(--color-muted);
130
130
  }
131
131
 
@@ -195,7 +195,7 @@
195
195
  border: 1px solid var(--color-line);
196
196
  border-radius: var(--radius-xl);
197
197
  overflow: hidden;
198
- box-shadow: 0 24px 64px -16px oklch(0.15 0.010 30 / 0.12);
198
+ box-shadow: var(--shadow-xl);
199
199
  position: relative;
200
200
  }
201
201
 
@@ -48,7 +48,7 @@
48
48
  border-radius: var(--radius-sm, 2px);
49
49
  border: 1px solid var(--color-line, oklch(0.85 0.02 80));
50
50
  background: var(--color-bg, oklch(0.97 0.01 80));
51
- box-shadow: 0 10px 38px -10px oklch(0.2 0.02 80 / 0.25), 0 10px 20px -15px oklch(0.2 0.02 80 / 0.15);
51
+ box-shadow: var(--shadow-xl);
52
52
  padding: 0.25rem;
53
53
  }
54
54
 
@@ -146,7 +146,7 @@
146
146
  .ms-featured-check {
147
147
  flex-shrink: 0;
148
148
  margin-top: 0.25rem;
149
- color: var(--color-accent, oklch(0.45 0.18 25));
149
+ color: var(--color-accent);
150
150
  }
151
151
 
152
152
  /* Separator */
@@ -257,7 +257,7 @@
257
257
 
258
258
  .ms-model-check {
259
259
  flex-shrink: 0;
260
- color: var(--color-accent, oklch(0.45 0.18 25));
260
+ color: var(--color-accent);
261
261
  }
262
262
 
263
263
  .ms-model-meta {
@@ -9,7 +9,7 @@ import { Icon } from "../icon/Icon";
9
9
  /**
10
10
  * navItemClass — 导航项统一样式(全域导航一致)。
11
11
  *
12
- * active 时深墨 pill 高亮(bg-ink text-white),其余 hover 浮现。
12
+ * active 时深墨 pill 高亮(bg-ink text-bg,深浅主题自适应),其余 hover 浮现。
13
13
  * 链接本身由调用方渲染(Next.js 用 next/link,保持 SPA 导航),
14
14
  * 只要都套这个 className,任何站点的导航观感就完全一致。
15
15
  */
@@ -17,7 +17,7 @@ export function navItemClass(active: boolean): string {
17
17
  return cn(
18
18
  "inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium transition-colors",
19
19
  active
20
- ? "bg-ink text-white"
20
+ ? "bg-ink text-bg"
21
21
  : "text-ink-2 hover:bg-surface-2 hover:text-ink"
22
22
  );
23
23
  }
@@ -4,7 +4,7 @@
4
4
  .zmz-reasoning { border-left: 2px solid var(--color-line); padding-left: 0.6rem; }
5
5
  .zmz-reasoning-toggle { display: inline-flex; align-items: center; gap: 0.3rem; background: none; border: 0; cursor: pointer; font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-muted); padding: 0.15rem 0; }
6
6
  .zmz-reasoning-dot { display: inline-block; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--color-muted); }
7
- .zmz-reasoning-dot.live { background: var(--color-accent); animation: zmz-reasoning-pulse 1.2s ease-in-out infinite; }
7
+ .zmz-reasoning-dot.live { background: var(--color-live); animation: zmz-reasoning-pulse 1.2s ease-in-out infinite; }
8
8
  .zmz-chevron { transition: transform 0.15s var(--ease-out); }
9
9
  .zmz-chevron.open { transform: rotate(180deg); }
10
10
  .zmz-reasoning-body { margin: 0.4rem 0 0; overflow: hidden; font-size: 0.75rem; color: var(--color-muted); white-space: pre-wrap; }
@@ -16,7 +16,7 @@ export interface StickyBannerProps extends HTMLAttributes<HTMLDivElement> {
16
16
 
17
17
  const variantStyles: Record<BannerVariant, string> = {
18
18
  info: "bg-surface text-ink border-line",
19
- warning: "bg-ink text-white border-ink",
19
+ warning: "bg-ink text-bg border-ink",
20
20
  danger: "bg-bg text-ink border-ink border-2",
21
21
  };
22
22
 
@@ -1,11 +1,11 @@
1
1
  /* SubtaskPart — 执行树中的子任务行。token 依赖消费端 @theme
2
- (--color-line、--color-accent、--color-muted、--font-mono)。
2
+ (--color-line、--color-live、--color-muted、--font-mono)。
3
3
  执行树分支线(::before)等上下文增强由消费端自己的 CSS 叠加。 */
4
4
 
5
5
  .subtask-row { position: relative; display: flex; align-items: center; gap: 0.5rem; min-height: 2.15rem; padding: 0.38rem 0.4rem; background: transparent; }
6
6
  /* 执行树分支线(挂在左侧时间轴上;独立使用时容器需留出左侧空间) */
7
7
  .subtask-row::before { position: absolute; top: 1.08rem; left: -0.81rem; width: 0.81rem; height: 1px; background: var(--color-line); content: ""; }
8
- .subtask-icon { display: grid; width: 1rem; height: 1rem; place-items: center; color: var(--color-accent); }
8
+ .subtask-icon { display: grid; width: 1rem; height: 1rem; place-items: center; color: var(--color-live); }
9
9
  .subtask-copy { display: grid; min-width: 0; flex: 1; gap: 0.1rem; }
10
10
  .subtask-copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; font-weight: 600; }
11
11
  .subtask-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-muted); font-family: var(--font-mono); font-size: 0.6rem; }
@@ -20,13 +20,13 @@
20
20
  .zmz-task-node-trigger:disabled { cursor: default; }
21
21
  .zmz-task-node-trigger:disabled:hover { background: transparent; }
22
22
  .zmz-task-node-marker { display: inline-flex; width: 0.9rem; height: 0.9rem; align-items: center; justify-content: center; border: 1px solid var(--color-line); border-radius: 50%; background: var(--color-surface); color: var(--color-success); }
23
- .zmz-task-node.in_progress .zmz-task-node-marker { border-color: var(--color-accent); }
23
+ .zmz-task-node.in_progress .zmz-task-node-marker { border-color: var(--color-live); }
24
24
  .zmz-task-node-copy { display: flex; min-width: 0; align-items: baseline; gap: 0.45rem; }
25
25
  .zmz-task-node-copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.76rem; font-weight: 600; }
26
26
  .zmz-task-node-index { flex: 0 0 auto; color: var(--color-muted); font-family: var(--font-mono); font-size: 0.55rem; }
27
27
  .zmz-task-node.completed .zmz-task-node-copy strong { color: var(--color-muted); text-decoration: line-through; }
28
- .zmz-task-node.in_progress .zmz-task-node-copy strong { color: var(--color-accent-strong); }
29
- .zmz-todo-spinner { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: var(--color-accent); animation: zmz-pulse 1s var(--ease-out) infinite; }
28
+ .zmz-task-node.in_progress .zmz-task-node-copy strong { color: var(--color-live); }
29
+ .zmz-todo-spinner { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: var(--color-live); animation: zmz-pulse 1s var(--ease-out) infinite; }
30
30
  .zmz-chevron { transition: transform 0.15s var(--ease-out); }
31
31
  .zmz-chevron.open { transform: rotate(180deg); }
32
32
  .zmz-task-executions { position: relative; display: grid; gap: 0; margin: 0 0 0.15rem 1.3rem; padding: 0.05rem 0 0.1rem 1rem; }
@@ -8,9 +8,9 @@
8
8
  .tool-card-trigger { display: flex; width: 100%; min-height: 2.15rem; align-items: center; gap: 0.5rem; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--color-ink); padding: 0.38rem 0.4rem; text-align: left; cursor: pointer; transition: background 0.15s var(--ease-out); }
9
9
  .tool-card-trigger:hover { background: var(--color-surface); }
10
10
  .tool-card-glyph { display: grid; width: 1rem; height: 1rem; flex: 0 0 1rem; place-items: center; border-radius: 50%; color: var(--color-muted); }
11
- .tool-card.running .tool-card-glyph { color: var(--color-accent); animation: zmz-tool-pulse 1s ease-in-out infinite; }
11
+ .tool-card.running .tool-card-glyph { color: var(--color-live); animation: zmz-tool-pulse 1s ease-in-out infinite; }
12
12
  .tool-card.completed .tool-card-glyph { color: var(--color-success); }
13
- .tool-card.failed .tool-card-glyph { color: #b03a28; }
13
+ .tool-card.failed .tool-card-glyph { color: var(--color-danger); }
14
14
  .tool-card-label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; }
15
15
  .tool-card-duration { flex: 0 0 auto; color: var(--color-muted); font-family: var(--font-mono); font-size: 0.6rem; }
16
16
  .tool-card-chevron { flex: 0 0 auto; color: var(--color-muted); transition: transform 0.15s var(--ease-out); }
@@ -19,7 +19,7 @@
19
19
  .tool-card-detail-section { display: grid; gap: 0.25rem; }
20
20
  .tool-card-detail-label { color: var(--color-muted); font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.08em; text-transform: uppercase; }
21
21
  .tool-card-detail pre { margin: 0; color: var(--color-muted); font-family: var(--font-mono); font-size: 0.68rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
22
- .tool-card-live-note { color: var(--color-accent-strong); font-family: var(--font-mono); font-size: 0.62rem; }
22
+ .tool-card-live-note { color: var(--color-live); font-family: var(--font-mono); font-size: 0.62rem; }
23
23
 
24
24
  .zmz-tool-group { display: grid; gap: 0.5rem; margin: 0.4rem 0; }
25
25
  .zmz-tool-group-trigger { display: flex; align-items: center; gap: 0.5rem; width: 100%; border: 1px solid var(--color-line); border-radius: var(--radius-sm); background: color-mix(in oklab, var(--color-surface) 92%, var(--color-bg)); color: var(--color-ink); padding: 0.4rem 0.6rem; cursor: pointer; transition: border-color 0.15s var(--ease-out); }
@@ -145,7 +145,7 @@ export function VanishInput({
145
145
  <button
146
146
  type="submit"
147
147
  aria-label="提交"
148
- className="ml-2 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ink text-white transition-transform hover:scale-105 active:scale-95"
148
+ className="ml-2 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ink text-bg transition-transform hover:scale-105 active:scale-95"
149
149
  >
150
150
  <svg
151
151
  className="h-3.5 w-3.5"
@@ -6,69 +6,134 @@
6
6
  * @import "@zmzai/theme/tokens";
7
7
  * @import "@zmzai/theme/fonts";
8
8
  *
9
- * v0.6.0 起为纯白 + 荧光绿(Pure white + fluorescent green),
10
- * zmzai.cloud 主应用(Hallmark Study)同源。
11
- * v0.2.0~0.5.x 为暖色杂志风(Warm paper + seal-red,已弃用)。
12
- * v0.1.x 黑白 Monochrome 风,更早废弃。
9
+ * v0.7.0 「墨骨 Ink Frame」—— 结构色与语义色分离
10
+ * · accent 从荧光绿改为墨黑,只承担 UI 结构(按钮 / 焦点 / 选中 / 硬线)
11
+ * · 绿退为品牌色 + 语义色(成功 / 运行中),不再当万能强调色
12
+ * · 中性灰阶统一色相 hue 265、彩度压到 0.008 以下,不再泛脏
13
+ * · 补齐 elevation 阴影体系(此前包内一个 shadow token 都没有)
14
+ *
15
+ * 沿革:
16
+ * v0.1.x 黑白 Monochrome
17
+ * v0.2~0.5 暖纸 + 印章红
18
+ * v0.6.x 纯白 + 荧光绿(accent 一色包打天下,装饰过载 → 观感 demo)
19
+ * v0.7.0 纯白 + 墨黑结构 + 绿色语义(现行)
13
20
  */
14
21
 
15
22
  @theme {
16
- /* ===== Color =====
17
- 纯白底 + 荧光绿 accent(与 zmzai.cloud 主应用同源,Hallmark Study 锁定)。
18
- accent 为荧光绿(oklch 0.78 0.220 142);accent-ink 为深色(用于绿底上的字)。 */
19
- --color-bg: oklch(0.995 0.002 90);
20
- --color-surface: oklch(0.98 0.004 90);
21
- --color-surface-2: oklch(0.95 0.006 88);
22
- --color-ink: oklch(0.15 0.010 30);
23
- --color-ink-2: oklch(0.50 0.012 55);
24
- --color-ink-3: oklch(0.55 0.012 55);
25
- --color-line: oklch(0.90 0.005 88);
26
- --color-line-strong: oklch(0.15 0.010 30);
27
- --color-accent: oklch(0.78 0.220 142);
28
- --color-accent-ink: oklch(0.15 0.010 30);
29
- --color-success: oklch(0.72 0.180 145);
30
- --color-danger: oklch(0.55 0.200 25);
31
- --color-warning: oklch(0.80 0.150 90);
32
-
33
- /* Dark surface — 仅用于代码块/preview,不是全局 dark mode */
34
- --color-dark-bg: #0A0A0A;
35
- --color-dark-surface: #141414;
36
- --color-dark-ink: #FAFAFA;
37
- --color-dark-line: #27272A;
38
-
39
- /* ===== Fonts =====
40
- 无衬线主字体(MiSans,中文现代感)+ Noto Serif SC 标题衬线 + JetBrains Mono 等宽。
41
- v0.2.0~0.2.8 曾以 Noto Serif SC 为 sans,但 next/font 只加载 latin 子集、
42
- 中文 fallback 系统宋体观感陈旧——0.2.9 改回 MiSans。 */
43
- --font-sans: "MiSans", "Noto Serif SC", "Source Han Serif SC", "PingFang SC", "Microsoft YaHei", sans-serif;
23
+ /* ══════════════════════════════════════════════════════
24
+ 1. Neutral 中性灰阶
25
+ 统一色相 hue 265(冷中性),彩度随明度轻微起伏但全部 0.017。
26
+ v0.6 的 ink(30°) / ink-2(55°) / line(88°) / surface(90°) 四个色相
27
+ 混用,是"说不出哪里怪"的根源。
28
+ ══════════════════════════════════════════════════════ */
29
+ --color-bg: oklch(1 0 0); /* 页面底,纯白 */
30
+ --color-surface: oklch(0.985 0.001 265); /* 卡片 / 面板 */
31
+ --color-surface-2: oklch(0.968 0.002 265); /* 嵌套 / 表头 */
32
+ --color-surface-3: oklch(0.94 0.003 265); /* hover / 按下 */
33
+ --color-ink: oklch(0.21 0.008 265); /* 标题 / 正文 */
34
+ --color-ink-2: oklch(0.442 0.017 265); /* 次级文字 */
35
+ --color-ink-3: oklch(0.617 0.014 265); /* 辅助 / 占位 · 3.69:1 */
36
+ --color-line: oklch(0.92 0.004 265); /* 常规边框 */
37
+ --color-line-strong: oklch(0.87 0.005 265); /* 强调边框 */
38
+ --color-rule: oklch(0.21 0.008 265); /* 硬分隔线 = ink */
39
+
40
+ /* ══════════════════════════════════════════════════════
41
+ 2. Structure accent — 墨黑
42
+ 只干结构的活:主按钮、焦点环、选中态、硬线、链接 hover。
43
+ 高饱和彩色的活它不干——那正是 v0.6 显得 demo 的原因。
44
+ ══════════════════════════════════════════════════════ */
45
+ --color-accent: oklch(0.21 0.008 265);
46
+ --color-accent-ink: oklch(1 0 0);
47
+ --color-accent-strong: oklch(0.09 0.004 265);
48
+ --color-accent-tint: oklch(0.968 0.002 265);
49
+ --color-accent-readable: var(--color-accent);
50
+
51
+ /* ══════════════════════════════════════════════════════
52
+ 3. Brand green — 品牌资产专用
53
+ 只给 favicon / logo / 印章 / 极少数品牌位。
54
+ 不承担交互语义,不进按钮、不进边框。
55
+ ══════════════════════════════════════════════════════ */
56
+ --color-brand: oklch(0.499 0.14 145); /* 5.67:1 */
57
+ --color-brand-tint: oklch(0.955 0.035 145);
58
+
59
+ /* ══════════════════════════════════════════════════════
60
+ 4. Semantic — 语义色
61
+ 只表达状态,不做装饰。每色配一档浅底 tint 用于 badge / 提示条。
62
+ 全部经 WCAG 校验:文字用途 ≥ 4.5:1(白底)。
63
+ 对照:v0.6 的荧光绿在白底只有 1.8:1,21 处 text-accent 全部不达标。
64
+ ══════════════════════════════════════════════════════ */
65
+ --color-live: oklch(0.499 0.17 145); /* 运行中 / 活跃 · 5.55:1 */
66
+ --color-success: oklch(0.499 0.13 145); /* 成功 · 5.70:1 */
67
+ --color-success-tint: oklch(0.955 0.035 145);
68
+ --color-warning: oklch(0.499 0.12 75); /* 告警 · 6.14:1 */
69
+ --color-warning-tint: oklch(0.955 0.045 90);
70
+ --color-danger: oklch(0.499 0.18 25); /* 错误 · 6.62:1 */
71
+ --color-danger-tint: oklch(0.955 0.03 25);
72
+
73
+ /* ══════════════════════════════════════════════════════
74
+ 5. Dark surface — 仅代码块 / preview,不是全局 dark mode
75
+ ══════════════════════════════════════════════════════ */
76
+ --color-dark-bg: oklch(0.17 0.006 265);
77
+ --color-dark-surface: oklch(0.23 0.007 265);
78
+ --color-dark-ink: oklch(0.97 0.002 265);
79
+ --color-dark-line: oklch(0.32 0.008 265);
80
+
81
+ /* ══════════════════════════════════════════════════════
82
+ 6. Elevation — 阴影
83
+ v0.6 完全没有 shadow token,各站只能手写 oklch(... / 0.12) 硬编码。
84
+ 统一用 ink 色做阴影基色,避免出现彩虹灰。
85
+ ══════════════════════════════════════════════════════ */
86
+ --shadow-xs: 0 1px 2px oklch(0.21 0.008 265 / 0.05);
87
+ --shadow-sm: 0 1px 3px oklch(0.21 0.008 265 / 0.06),
88
+ 0 1px 2px oklch(0.21 0.008 265 / 0.04);
89
+ --shadow-md: 0 4px 12px -2px oklch(0.21 0.008 265 / 0.08),
90
+ 0 2px 4px -2px oklch(0.21 0.008 265 / 0.05);
91
+ --shadow-lg: 0 12px 32px -8px oklch(0.21 0.008 265 / 0.12),
92
+ 0 4px 8px -4px oklch(0.21 0.008 265 / 0.06);
93
+ --shadow-xl: 0 24px 56px -12px oklch(0.21 0.008 265 / 0.16);
94
+
95
+ /* ══════════════════════════════════════════════════════
96
+ 7. Fonts
97
+ MiSans 主字体;fallback 全部为无衬线。
98
+ v0.6 把 Noto Serif SC 排在 sans 第二顺位——MiSans CDN 一挂整站
99
+ 掉宋体。此处移除。
100
+ ══════════════════════════════════════════════════════ */
101
+ --font-sans: "MiSans", -apple-system, BlinkMacSystemFont, "PingFang SC",
102
+ "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
44
103
  --font-serif: "Noto Serif SC", "Source Han Serif SC", "Songti SC", serif;
45
- --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;
46
-
47
- /* ===== Radius =====
48
- 锐利编辑风(sharp editorial)。 */
49
- --radius-sm: 2px;
50
- --radius: 2px;
51
- --radius-lg: 4px;
52
- --radius-xl: 6px;
104
+ --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", "Menlo", monospace;
105
+
106
+ /* ══════════════════════════════════════════════════════
107
+ 8. Radius — 锐利但不寒酸
108
+ v0.6 的 2/2/4/6 太扁,大容器用 2px 像表格不成卡片。
109
+ ══════════════════════════════════════════════════════ */
110
+ --radius-xs: 2px;
111
+ --radius-sm: 3px;
112
+ --radius: 4px;
113
+ --radius-md: 4px;
114
+ --radius-lg: 6px;
115
+ --radius-xl: 8px;
116
+ --radius-2xl: 12px;
53
117
  --radius-pill: 9999px;
54
118
 
55
- /* ===== Motion ===== */
119
+ /* ══════════════════════════════════════════════════════
120
+ 9. Motion
121
+ ══════════════════════════════════════════════════════ */
56
122
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
57
123
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
58
124
 
59
- /* ===== Backward-compat aliases =====
60
- Maps old v1 token names → new v2 values.
61
- Lets zmzai-agent's 267 existing CSS classes work without modification. */
62
- --color-paper: var(--color-bg);
63
- --color-muted: var(--color-ink-2);
64
- --color-rule: var(--color-ink);
125
+ /* ══════════════════════════════════════════════════════
126
+ 10. Backward-compat aliases
127
+ v1 token v0.7 值,让既有 267 class 无需改动。
128
+ ══════════════════════════════════════════════════════ */
129
+ --color-paper: var(--color-bg);
130
+ --color-muted: var(--color-ink-2);
65
131
  --color-surface-strong: var(--color-surface-2);
66
- --color-accent-strong: oklch(0.68 0.220 142);
67
- --animate-dur-fast: 150ms;
68
- --animate-dur-base: 250ms;
69
- --animate-dur-slow: 500ms;
70
- --radius-card: var(--radius);
71
- --radius-input: var(--radius-sm);
132
+ --animate-dur-fast: 150ms;
133
+ --animate-dur-base: 250ms;
134
+ --animate-dur-slow: 500ms;
135
+ --radius-card: var(--radius);
136
+ --radius-input: var(--radius-sm);
72
137
  }
73
138
 
74
139
  :root {