@zmzai/theme 0.3.0 → 0.4.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.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "zmzai 全品牌设计系统 — Radix UI + framer-motion + Tailwind v4 + MiSans",
@@ -48,6 +48,10 @@
48
48
  "./brand": {
49
49
  "types": "./src/brand/index.ts",
50
50
  "import": "./src/brand/index.ts"
51
+ },
52
+ "./parse-unified-diff": {
53
+ "types": "./src/components/diff-view/parse-unified-diff.ts",
54
+ "import": "./src/components/diff-view/parse-unified-diff.ts"
51
55
  }
52
56
  },
53
57
  "files": [
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ import { Icon } from "../../components/icon/Icon";
6
+ import "./artifact-card.css";
7
+
8
+ export interface ArtifactCardProps {
9
+ /** 产物路径(文件名) */
10
+ path: string;
11
+ /** 元信息行(如 "pptx · 1.2 MiB") */
12
+ meta: string;
13
+ /** 有预览时显示的提示角标 */
14
+ previewHint?: boolean;
15
+ /** 下载直链(无则不渲染下载按钮) */
16
+ downloadUrl?: string;
17
+ /** 点击卡片(打开预览) */
18
+ onOpen?: () => void;
19
+ }
20
+
21
+ /** MIME → 卡片短类型名;未知长类型取 subtype 末段。 */
22
+ export function shortContentType(contentType: string): string {
23
+ const type = contentType.split(";")[0]!.trim().toLowerCase();
24
+ const known: Record<string, string> = {
25
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
26
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow": "ppsx",
27
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
28
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
29
+ "application/pdf": "pdf",
30
+ "application/json": "json",
31
+ "application/zip": "zip",
32
+ "text/html": "html",
33
+ "text/markdown": "md",
34
+ "text/plain": "txt",
35
+ "text/css": "css",
36
+ };
37
+ if (known[type]) return known[type]!;
38
+ const sub = type.split("/").pop() ?? "file";
39
+ if (sub.length <= 14) return sub;
40
+ const tail = sub.split(".").pop() ?? sub;
41
+ return tail.length <= 14 ? tail : "file";
42
+ }
43
+
44
+ export function formatBytes(value: number): string {
45
+ if (value < 1024) return `${value} B`;
46
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`;
47
+ return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
48
+ }
49
+
50
+ /**
51
+ * ArtifactCard — 产物卡:路径 + 类型/大小 meta + 预览角标 + 下载。
52
+ *
53
+ * hover 抬升 + 印章红描边渐现(炫酷),入场淡入。
54
+ * 显式 grid 定位防超长 MIME 撑爆布局。
55
+ *
56
+ * @example
57
+ * <ArtifactCard path="报告.pptx" meta={`${shortContentType(m)} · ${formatBytes(b)}`} previewHint onOpen={open} downloadUrl={url} />
58
+ */
59
+ export function ArtifactCard({ path, meta, previewHint, downloadUrl, onOpen }: ArtifactCardProps) {
60
+ return (
61
+ <motion.button
62
+ type="button"
63
+ className="zmz-artifact-card"
64
+ onClick={onOpen}
65
+ initial={{ opacity: 0, y: 6 }}
66
+ animate={{ opacity: 1, y: 0 }}
67
+ transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
68
+ whileHover={{ y: -2 }}
69
+ >
70
+ <span className="zmz-artifact-name">{path}</span>
71
+ <span className="zmz-artifact-meta">{meta}</span>
72
+ <span className="zmz-artifact-actions">
73
+ {previewHint && <span className="zmz-artifact-preview-hint">预览</span>}
74
+ {downloadUrl && (
75
+ <a className="zmz-artifact-download" href={downloadUrl} onClick={(event) => event.stopPropagation()}>
76
+ <Icon name="download" size={12} />
77
+ </a>
78
+ )}
79
+ </span>
80
+ </motion.button>
81
+ );
82
+ }
@@ -0,0 +1,12 @@
1
+ /* ArtifactCard — 产物卡。token 依赖消费端 @theme。 */
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); }
4
+ /* 显式 grid 定位:name/meta 固定在左列(1fr),actions 固定右列跨两行。
5
+ 自动放置曾把超长 MIME 文本(无空格、无断行)排进右列 auto 轨道,
6
+ 撑爆布局把名字列压成 0 → 文件名竖排逐字折行。 */
7
+ .zmz-artifact-name { grid-column: 1; grid-row: 1; font-family: var(--font-mono); font-size: 0.75rem; overflow-wrap: anywhere; }
8
+ .zmz-artifact-meta { grid-column: 1; grid-row: 2; font-size: 0.6875rem; color: var(--color-muted); overflow-wrap: anywhere; }
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); }
11
+ .zmz-artifact-download { display: inline-flex; color: var(--color-muted); }
12
+ .zmz-artifact-download:hover { color: var(--color-accent); }
@@ -0,0 +1 @@
1
+ export * from "./ArtifactCard";
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
+
6
+ import { Icon } from "../../components/icon/Icon";
7
+ import { DiffView } from "../../components/diff-view";
8
+ import "./edit-card.css";
9
+
10
+ /**
11
+ * EditCard — 文件改动卡:路径 + 修订号头部,展开看彩色 diff。
12
+ *
13
+ * 展开内容用 theme DiffView 渲染,AnimatePresence 渐显。
14
+ *
15
+ * @example
16
+ * <EditCard path="src/app.ts" revision="rev_abc123" diff={unifiedDiff} />
17
+ */
18
+ export function EditCard({ path, revision, diff }: { path: string; revision: string; diff: string }) {
19
+ const [open, setOpen] = useState(false);
20
+ return (
21
+ <div className="zmz-edit-card">
22
+ <button type="button" className="zmz-edit-head" onClick={() => setOpen((value) => !value)}>
23
+ <Icon name="chevron-down" size={11} className={open ? "zmz-chevron open" : "zmz-chevron"} />
24
+ <span className="zmz-edit-path">{path}</span>
25
+ <small>{revision.slice(0, 12)}</small>
26
+ </button>
27
+ <AnimatePresence initial={false}>
28
+ {open && (
29
+ <motion.div
30
+ className="zmz-edit-body"
31
+ initial={{ opacity: 0, height: 0 }}
32
+ animate={{ opacity: 1, height: "auto" }}
33
+ exit={{ opacity: 0, height: 0 }}
34
+ transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
35
+ >
36
+ <DiffView diff={diff} />
37
+ </motion.div>
38
+ )}
39
+ </AnimatePresence>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,9 @@
1
+ /* EditCard — 文件改动卡。token 依赖消费端 @theme。 */
2
+ .zmz-edit-card { border: 1px solid var(--color-line); border-radius: var(--radius); background: var(--color-surface); overflow: hidden; }
3
+ .zmz-edit-head { display: flex; align-items: center; gap: 0.5rem; width: 100%; text-align: left; background: none; border: 0; cursor: pointer; padding: 0.55rem 0.75rem; font: inherit; color: inherit; }
4
+ .zmz-edit-head:hover { background: var(--color-surface-strong); }
5
+ .zmz-edit-path { font-family: var(--font-mono); font-size: 0.75rem; flex: 1; overflow-wrap: anywhere; }
6
+ .zmz-edit-head small { color: var(--color-muted); font-family: var(--font-mono); font-size: 0.625rem; }
7
+ .zmz-edit-body { overflow: hidden; padding: 0 0.6rem 0.6rem; }
8
+ .zmz-chevron { transition: transform 0.15s var(--ease-out); }
9
+ .zmz-chevron.open { transform: rotate(180deg); }
@@ -0,0 +1 @@
1
+ export * from "./EditCard";
@@ -49,6 +49,11 @@ export * from "./multi-step-loader";
49
49
  export * from "./timeline";
50
50
 
51
51
  // === Business (agent workflow) ===
52
+ export * from "./message";
53
+ export * from "./reasoning";
54
+ export * from "./artifact-card";
55
+ export * from "./edit-card";
56
+ export * from "./markdown";
52
57
  export * from "./markdown";
53
58
  export * from "./diff-view";
54
59
  export * from "./tool-card";
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import { motion } from "framer-motion";
5
+
6
+ import { Badge } from "../../components/badge";
7
+ import "./message.css";
8
+
9
+ export interface MessageItemProps {
10
+ /** 发送方角色:决定头像配色与气泡方向 */
11
+ role: "user" | "assistant";
12
+ /** 头像字(单字,如 "你"/"使")或名字首字 */
13
+ avatar: string;
14
+ /** 显示名(meta 行) */
15
+ name: string;
16
+ /** 右侧状态(assistant 执行中/已完成) */
17
+ status?: { active: boolean };
18
+ /** 时间标签(HH:MM) */
19
+ time?: string | null;
20
+ /** 消息体(文本/执行树/工具组等任意内容) */
21
+ children: ReactNode;
22
+ /** 关闭入场动画(流式追加频繁时防闪) */
23
+ noMotion?: boolean;
24
+ }
25
+
26
+ /**
27
+ * MessageItem — 消息行:头像 + 名字/状态/时间 meta + 内容体。
28
+ *
29
+ * user 气泡为描边卡片,assistant 为开放排版(适合挂执行树)。
30
+ * 默认 framer-motion 淡入上移入场(流式追加传 noMotion 关闭)。
31
+ *
32
+ * @example
33
+ * <MessageItem role="assistant" avatar="使" name="Agent" status={{ active: true }}>
34
+ * <Markdown text={reply} />
35
+ * </MessageItem>
36
+ */
37
+ export function MessageItem({ role, avatar, name, status, time, children, noMotion }: MessageItemProps) {
38
+ const body = (
39
+ <>
40
+ <span className={`zmz-message-avatar ${role === "assistant" ? "assistant" : ""}`}>{avatar}</span>
41
+ <div className="zmz-message-column">
42
+ <div className="zmz-message-meta">
43
+ <strong>{name}</strong>
44
+ {status && <Badge variant={status.active ? "accent" : "success"} size="sm">{status.active ? "执行中" : "已完成"}</Badge>}
45
+ {time && <span className="zmz-message-time">{time}</span>}
46
+ </div>
47
+ {children}
48
+ </div>
49
+ </>
50
+ );
51
+ if (noMotion) return <div className={`zmz-message ${role}`}>{body}</div>;
52
+ return (
53
+ <motion.div
54
+ className={`zmz-message ${role}`}
55
+ initial={{ opacity: 0, y: 8 }}
56
+ animate={{ opacity: 1, y: 0 }}
57
+ transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
58
+ >
59
+ {body}
60
+ </motion.div>
61
+ );
62
+ }
63
+
64
+ /**
65
+ * MessageStream — 消息流容器:统一行距 + 滚动区。
66
+ *
67
+ * @example
68
+ * <MessageStream scrollRef={ref}>{messages.map(...)}</MessageStream>
69
+ */
70
+ export function MessageStream({ children, scrollRef, className }: { children: ReactNode; scrollRef?: React.Ref<HTMLDivElement>; className?: string }) {
71
+ return (
72
+ <div ref={scrollRef} className={`zmz-message-stream ${className ?? ""}`}>
73
+ {children}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1 @@
1
+ export * from "./Message";
@@ -0,0 +1,11 @@
1
+ /* Message — 消息流。token 依赖消费端 @theme。 */
2
+ .zmz-message { display: flex; gap: 0.55rem; }
3
+ .zmz-message-avatar { display: grid; width: 1.5rem; height: 1.5rem; flex: 0 0 1.5rem; place-items: center; border-radius: 50%; background: var(--color-surface-strong); color: var(--color-accent-strong); font-family: var(--font-mono); font-size: 0.6rem; font-weight: 700; }
4
+ .zmz-message-avatar.assistant { background: var(--color-accent); color: var(--color-accent-ink); }
5
+ .zmz-message-column { display: grid; min-width: 0; flex: 1; gap: 0.55rem; }
6
+ .zmz-message-meta { display: flex; align-items: baseline; gap: 0.55rem; min-height: 1.5rem; }
7
+ .zmz-message-meta strong { font-size: 0.75rem; font-weight: 700; color: var(--color-ink); }
8
+ .zmz-message-time { margin-left: auto; color: var(--color-muted); font-family: var(--font-mono); font-size: 0.6rem; }
9
+ .zmz-message-content { min-width: 0; color: var(--color-ink); font-size: 0.875rem; line-height: 1.65; overflow-wrap: anywhere; }
10
+ .zmz-message.user .zmz-message-content { padding: 0.65rem 0.8rem; border: 1px solid var(--color-line); border-radius: var(--radius); background: var(--color-surface); }
11
+ .zmz-message-stream { display: grid; gap: 0.9rem; align-content: start; overflow-y: auto; min-height: 0; }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
+
6
+ import { Icon } from "../../components/icon/Icon";
7
+ import "./reasoning.css";
8
+
9
+ /**
10
+ * Reasoning — 思考过程折叠块。
11
+ *
12
+ * 运行中自动展开(呼吸圆点),完成后收起为标签行,点击回看。
13
+ * 展开内容渐显(AnimatePresence)。
14
+ *
15
+ * @example
16
+ * <Reasoning text={chainOfThought} active={isStreaming} />
17
+ */
18
+ export function Reasoning({ text, active = false }: { text: string; active?: boolean }) {
19
+ const [open, setOpen] = useState(false);
20
+ const expanded = active || open;
21
+ return (
22
+ <div className="zmz-reasoning">
23
+ <button type="button" className="zmz-reasoning-toggle" aria-expanded={expanded} onClick={() => setOpen((value) => !value)}>
24
+ <span className={`zmz-reasoning-dot ${active ? "live" : ""}`} aria-hidden />
25
+ <span>思考过程</span>
26
+ <Icon name="chevron-down" size={11} className={expanded ? "zmz-chevron open" : "zmz-chevron"} />
27
+ </button>
28
+ <AnimatePresence initial={false}>
29
+ {expanded && (
30
+ <motion.pre
31
+ className="zmz-reasoning-body"
32
+ initial={{ opacity: 0, height: 0 }}
33
+ animate={{ opacity: 1, height: "auto" }}
34
+ exit={{ opacity: 0, height: 0 }}
35
+ transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
36
+ >
37
+ {text}
38
+ </motion.pre>
39
+ )}
40
+ </AnimatePresence>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1 @@
1
+ export * from "./Reasoning";
@@ -0,0 +1,10 @@
1
+ /* Reasoning — 思考过程折叠。token 依赖消费端 @theme。 */
2
+ @keyframes zmz-reasoning-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.75); } }
3
+
4
+ .zmz-reasoning { border-left: 2px solid var(--color-line); padding-left: 0.6rem; }
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
+ .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; }
8
+ .zmz-chevron { transition: transform 0.15s var(--ease-out); }
9
+ .zmz-chevron.open { transform: rotate(180deg); }
10
+ .zmz-reasoning-body { margin: 0.4rem 0 0; overflow: hidden; font-size: 0.75rem; color: var(--color-muted); white-space: pre-wrap; }