@changfenhuang/dsh-genui 0.9.1

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/LICENSE +21 -0
  3. package/README.md +261 -0
  4. package/README.zh-CN.md +261 -0
  5. package/SKILL.md +115 -0
  6. package/cordis.patch.yml +9 -0
  7. package/demo-prompts.md +118 -0
  8. package/lib/assets/echarts.js +64 -0
  9. package/lib/assets/mermaid.js +3591 -0
  10. package/lib/assets/three.js +3844 -0
  11. package/lib/client.js +5 -0
  12. package/lib/index.js +1895 -0
  13. package/lib/invariant.js +19 -0
  14. package/lib/types/client/EChartNode.d.ts +4 -0
  15. package/lib/types/client/ErrorBoundary.d.ts +30 -0
  16. package/lib/types/client/GenuiBlock.d.ts +7 -0
  17. package/lib/types/client/PlotBlock.d.ts +44 -0
  18. package/lib/types/client/action-context.d.ts +7 -0
  19. package/lib/types/client/asset-echarts.d.ts +15 -0
  20. package/lib/types/client/asset-loader.d.ts +31 -0
  21. package/lib/types/client/asset-mermaid.d.ts +1 -0
  22. package/lib/types/client/asset-three.d.ts +1 -0
  23. package/lib/types/client/blocks/advanced.d.ts +104 -0
  24. package/lib/types/client/blocks/basic.d.ts +27 -0
  25. package/lib/types/client/blocks/charts.d.ts +38 -0
  26. package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
  27. package/lib/types/client/blocks/diagram/index.d.ts +5 -0
  28. package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
  29. package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
  30. package/lib/types/client/blocks/forms.d.ts +83 -0
  31. package/lib/types/client/blocks/render-node.d.ts +10 -0
  32. package/lib/types/client/blocks/state.d.ts +58 -0
  33. package/lib/types/client/dom-fence.d.ts +14 -0
  34. package/lib/types/client/echarts-lazy.d.ts +17 -0
  35. package/lib/types/client/fence-render.d.ts +58 -0
  36. package/lib/types/client/gallery.d.ts +9 -0
  37. package/lib/types/client/guard.d.ts +126 -0
  38. package/lib/types/client/index.d.ts +36 -0
  39. package/lib/types/client/interaction-store.d.ts +43 -0
  40. package/lib/types/client/mermaid-core.d.ts +8 -0
  41. package/lib/types/client/mermaid-lazy.d.ts +9 -0
  42. package/lib/types/client/mermaid-safe.d.ts +52 -0
  43. package/lib/types/client/panel-command.d.ts +29 -0
  44. package/lib/types/client/panel-store.d.ts +94 -0
  45. package/lib/types/client/panel.d.ts +15 -0
  46. package/lib/types/client/parse-partial.d.ts +37 -0
  47. package/lib/types/client/safe-math.d.ts +43 -0
  48. package/lib/types/client/scene3d-core.d.ts +21 -0
  49. package/lib/types/client/scene3d-lazy.d.ts +8 -0
  50. package/lib/types/client/spec.d.ts +610 -0
  51. package/lib/types/client/toolview.d.ts +7 -0
  52. package/lib/types/index.d.ts +1 -0
  53. package/lib/types/plugin/index.d.ts +25 -0
  54. package/lib/types/plugin/invariant.d.ts +15 -0
  55. package/lib/types/plugin/tool.d.ts +27 -0
  56. package/lib/types/shared/fence-repair.d.ts +67 -0
  57. package/package.json +121 -0
  58. package/src/client/EChartNode.tsx +242 -0
  59. package/src/client/ErrorBoundary.tsx +66 -0
  60. package/src/client/GenuiBlock.module.css +1263 -0
  61. package/src/client/GenuiBlock.tsx +166 -0
  62. package/src/client/PlotBlock.module.css +155 -0
  63. package/src/client/PlotBlock.tsx +431 -0
  64. package/src/client/action-context.ts +32 -0
  65. package/src/client/asset-echarts.ts +25 -0
  66. package/src/client/asset-loader.ts +79 -0
  67. package/src/client/asset-mermaid.ts +13 -0
  68. package/src/client/asset-three.ts +12 -0
  69. package/src/client/blocks/advanced.tsx +435 -0
  70. package/src/client/blocks/basic.tsx +116 -0
  71. package/src/client/blocks/charts.tsx +319 -0
  72. package/src/client/blocks/diagram/geometry.ts +151 -0
  73. package/src/client/blocks/diagram/index.tsx +284 -0
  74. package/src/client/blocks/diagram/layout.ts +199 -0
  75. package/src/client/blocks/diagram/theme.ts +108 -0
  76. package/src/client/blocks/forms.tsx +523 -0
  77. package/src/client/blocks/render-node.tsx +260 -0
  78. package/src/client/blocks/state.ts +61 -0
  79. package/src/client/dom-fence.tsx +641 -0
  80. package/src/client/echarts-lazy.ts +35 -0
  81. package/src/client/fence-render.tsx +230 -0
  82. package/src/client/gallery.ts +128 -0
  83. package/src/client/guard.ts +1450 -0
  84. package/src/client/index.tsx +195 -0
  85. package/src/client/interaction-store.ts +122 -0
  86. package/src/client/mermaid-core.ts +121 -0
  87. package/src/client/mermaid-lazy.ts +31 -0
  88. package/src/client/mermaid-safe.ts +122 -0
  89. package/src/client/panel-command.ts +119 -0
  90. package/src/client/panel-store.ts +471 -0
  91. package/src/client/panel.tsx +185 -0
  92. package/src/client/parse-partial.ts +162 -0
  93. package/src/client/safe-math.ts +270 -0
  94. package/src/client/scene3d-core.ts +192 -0
  95. package/src/client/scene3d-lazy.ts +26 -0
  96. package/src/client/spec.ts +767 -0
  97. package/src/client/toolview.tsx +66 -0
  98. package/src/css-modules.d.ts +5 -0
  99. package/src/index.ts +12 -0
  100. package/src/plugin/index.ts +173 -0
  101. package/src/plugin/invariant.ts +30 -0
  102. package/src/plugin/node-shims.d.ts +31 -0
  103. package/src/plugin/tool.ts +330 -0
  104. package/src/shared/fence-repair.ts +244 -0
  105. package/tsconfig.json +35 -0
  106. package/tsdown.config.ts +193 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * The dsh-ui fence render pipeline, shared by both render channels:
3
+ *
4
+ * - **Registry channel** (contract hosts): the host's MarkdownText resolves
5
+ * ```dsh-ui fences through the fence-registry extension point and calls
6
+ * {@link renderGenuiFence} with a session-scoped context (sessionId + the
7
+ * settled source identity). An unrepairable body renders {@link FenceFallback}.
8
+ * - **DOM channel** (pristine hosts, no extension point): the DOM observer
9
+ * (`dom-fence.ts`) finds stock code blocks labelled `dsh-ui` and mounts
10
+ * {@link renderResolvedFenceNode} into its own React root, wrapped in the
11
+ * plugin-owned action context. An unrepairable body returns `null` so the
12
+ * stock code block stays visible.
13
+ *
14
+ * Structural types are declared locally on purpose: the context contract is
15
+ * a data shape, and pristine hosts do not export the host-side type names.
16
+ */
17
+ import { Fragment, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type Key, type ReactNode } from 'react'
18
+ import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives'
19
+ import { ErrorBoundary } from './ErrorBoundary.tsx'
20
+ import { GenuiBlock } from './GenuiBlock.tsx'
21
+ import { repairGenuiSpec } from './guard.ts'
22
+ import { fenceStateKey } from './interaction-store.ts'
23
+ import { parsePartialGenuiSpec } from './parse-partial.ts'
24
+ import { applyPanelOperation, diagnosePanelBudget, type PanelOperationStatus } from './panel-store.ts'
25
+ import type { GenuiSpec } from './spec.ts'
26
+ import { completeFenceJson, describeJsonFailure, isCompleteJson, repairFenceJson } from '../shared/fence-repair.ts'
27
+
28
+ /** Settled fence source identity (data shape, host-independent). */
29
+ export interface GenuiFenceSource {
30
+ /** Stable structural id, e.g. `['assistant', seq, block, fence]` or `dom:<anchor>:<i>`. */
31
+ readonly id: string
32
+ /** Three-part order: [messageSeq, textBlockIndex, fenceIndex]. */
33
+ readonly order: readonly [number, number, number]
34
+ }
35
+
36
+ /** Context a fence renderer receives beside the raw source and React key. */
37
+ export interface GenuiFenceContext {
38
+ /** Owning session route; absent outside a session-scoped render. */
39
+ readonly sessionId?: string
40
+ /** Present only for settled/interrupted renders with a stable identity. */
41
+ readonly source?: GenuiFenceSource
42
+ }
43
+
44
+ const FENCE_ERROR_STYLE: CSSProperties = {
45
+ margin: '0 0 6px',
46
+ padding: '6px 10px',
47
+ borderRadius: 6,
48
+ background: 'rgba(239, 68, 68, 0.14)',
49
+ border: '1px solid rgba(239, 68, 68, 0.4)',
50
+ color: '#f87171',
51
+ fontSize: 12,
52
+ lineHeight: 1.55,
53
+ whiteSpace: 'pre-wrap',
54
+ }
55
+
56
+ /**
57
+ * Fallback for a ```dsh-ui fence whose body has no finished component yet.
58
+ * Two very different situations land here and they must not be conflated:
59
+ *
60
+ * 1. **Streaming partial** — the reply is still being written and the JSON
61
+ * simply is not complete. The host marks the streaming message with
62
+ * `[data-streaming]` on the AssistantMarkdown root, which is an ancestor
63
+ * of every fence. While that marker is present, a plain code block is the
64
+ * correct rendering (partial JSON must never look like an error).
65
+ *
66
+ * 2. **Settled defect** — the message is finished but the body still does
67
+ * not parse as JSON (a malformed fence like a missing `}`). This used to
68
+ * fail silently: the fence degraded to a code block with no hint, and the
69
+ * author had no way to know the UI never rendered. Once the streaming
70
+ * marker is gone, surface a compact diagnostic with the parse position so
71
+ * the defect is visible instead of silent.
72
+ */
73
+ function FenceFallback({ raw, fenceKey }: { raw: string; fenceKey: Key }) {
74
+ const ref = useRef<HTMLDivElement | null>(null)
75
+ const [settled, setSettled] = useState(false)
76
+ useLayoutEffect(() => {
77
+ const node = ref.current
78
+ if (node !== null && node.closest('[data-streaming]') === null) setSettled(true)
79
+ })
80
+ const diagnostic = settled && raw.trim() !== '' ? describeJsonFailure(raw) : null
81
+ return (
82
+ <div ref={ref}>
83
+ {diagnostic !== null && (
84
+ <div style={FENCE_ERROR_STYLE} role="alert">
85
+ ⚠️ dsh-ui fence JSON 解析失败{diagnostic} —— 围栏保持为代码块;请让模型检查并修复 JSON 后重发。
86
+ </div>
87
+ )}
88
+ <CodeBlock key={fenceKey} code={`${raw}\n`} lang="dsh-ui" />
89
+ </div>
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Keyed publisher for a settled `panel:true` fence: submits ONE panel
95
+ * operation from the host-provided stable source (id + order), in an
96
+ * effect — never inside the render function. StrictMode's duplicate effects
97
+ * are absorbed by the operation map's per-source dedup, so the panel folds
98
+ * and notifies exactly once per source. Renders nothing.
99
+ */
100
+ function FencePanelPublisher({ sessionId, sourceId, order, spec }: {
101
+ sessionId: string
102
+ sourceId: string
103
+ order: readonly [number, number, number]
104
+ spec: GenuiSpec
105
+ }) {
106
+ useEffect(() => {
107
+ const status: PanelOperationStatus = applyPanelOperation(sessionId, {
108
+ sourceId,
109
+ order,
110
+ mode: spec.append === true ? 'append' : 'replace',
111
+ spec,
112
+ })
113
+ if (status === 'overflow') diagnosePanelBudget(sessionId, sourceId)
114
+ }, [sessionId, sourceId, order, spec])
115
+ return null
116
+ }
117
+
118
+ /**
119
+ * Resolve a raw fence body to a guarded spec.
120
+ *
121
+ * - Tier-1 repair (quote escape + trailing commas): safe at any time —
122
+ * adopted only when the whole body parses, so a still-growing streaming
123
+ * half keeps falling back to the code block, never flashing a banner.
124
+ * - Tier-2 completion (missing quotes/brackets): settled renders only —
125
+ * `context.source` exists exclusively once the message finished, so
126
+ * streaming halves are never completed early.
127
+ */
128
+ export function resolveGenuiSpec(raw: string, context?: GenuiFenceContext): GenuiSpec | null {
129
+ const parsed = parsePartialGenuiSpec(raw)
130
+ let spec = parsed === null ? null : repairGenuiSpec(parsed)
131
+ if (spec === null) {
132
+ const repaired = repairFenceJson(raw)
133
+ if (repaired !== null) {
134
+ const reparsed = parsePartialGenuiSpec(repaired.text)
135
+ spec = reparsed === null ? null : repairGenuiSpec(reparsed)
136
+ }
137
+ if (spec === null && context?.source !== undefined) {
138
+ const completed = completeFenceJson(raw)
139
+ if (completed !== null) {
140
+ const reparsed = parsePartialGenuiSpec(completed.text)
141
+ spec = reparsed === null ? null : repairGenuiSpec(reparsed)
142
+ }
143
+ }
144
+ }
145
+ return spec
146
+ }
147
+
148
+ /** The inline GenuiBlock tree for a resolved non-panel spec. */
149
+ function renderInlineFence(key: Key, context: GenuiFenceContext | undefined, spec: GenuiSpec): ReactNode {
150
+ const sessionId = context?.sessionId
151
+ return (
152
+ // React key carries the stable source identity when present (atomic
153
+ // remount at streaming→settled), falling back to the document key.
154
+ // Repaired specs render SILENTLY: once the UI renders, no amber note
155
+ // tells the user something was wrong — only an unrecoverable body keeps
156
+ // the red diagnostic.
157
+ <ErrorBoundary key={context?.source?.id ?? key} label="该界面">
158
+ <GenuiBlock
159
+ spec={spec}
160
+ // v2.7 durable state: session + stable source + content fingerprint —
161
+ // replaying the same content restores answers/lock/field values; new
162
+ // content (换题, edited spec) gets a fresh key. Without a stable
163
+ // source (streaming / non-conversation surfaces) state is not
164
+ // persisted.
165
+ stateKey={sessionId === undefined
166
+ ? undefined
167
+ : fenceStateKey(sessionId, context?.source?.id ?? String(key), JSON.stringify(spec))}
168
+ />
169
+ </ErrorBoundary>
170
+ )
171
+ }
172
+
173
+ /**
174
+ * The resolved fence render for the DOM channel: `null` when the body is
175
+ * unrepairable (the stock code block stays visible), otherwise the panel
176
+ * publisher (`panel:true`; renders nothing in the flow — mounted as an empty
177
+ * root so the taken-over block is hidden) or the inline GenuiBlock tree.
178
+ * Shared verbatim by both channels.
179
+ */
180
+ export function renderResolvedFenceNode(raw: string, key: Key, context?: GenuiFenceContext): ReactNode | null {
181
+ const spec = resolveGenuiSpec(raw, context)
182
+ if (spec === null) return null
183
+ if (spec.panel === true) {
184
+ // Publish only with a settled stable source — streaming/identity-less
185
+ // renders keep the panel untouched. Appends additionally gate on a
186
+ // complete body (a settled-but-malformed append never merges partial
187
+ // content).
188
+ if (context !== undefined && context.sessionId !== undefined && context.source !== undefined) {
189
+ if (spec.append === true && !isCompleteJson(raw)) return <Fragment key={key} />
190
+ return (
191
+ <FencePanelPublisher
192
+ key={key}
193
+ sessionId={context.sessionId}
194
+ sourceId={context.source.id}
195
+ order={context.source.order}
196
+ spec={spec}
197
+ />
198
+ )
199
+ }
200
+ return <Fragment key={key} />
201
+ }
202
+ return renderInlineFence(key, context, spec)
203
+ }
204
+
205
+ /**
206
+ * Registry-channel fence renderer (contract hosts): like the resolved node,
207
+ * but an unrepairable body renders the fallback code block + settled
208
+ * diagnostic — the host replaced its own block with our output — and an
209
+ * unpublishable `panel:true` fence renders `null` (nothing in the flow).
210
+ */
211
+ export function renderGenuiFence(raw: string, key: Key, context?: GenuiFenceContext): ReactNode {
212
+ const spec = resolveGenuiSpec(raw, context)
213
+ if (spec === null) return <FenceFallback key={key} fenceKey={key} raw={raw} />
214
+ if (spec.panel === true) {
215
+ if (context !== undefined && context.sessionId !== undefined && context.source !== undefined) {
216
+ if (spec.append === true && !isCompleteJson(raw)) return null
217
+ return (
218
+ <FencePanelPublisher
219
+ key={key}
220
+ sessionId={context.sessionId}
221
+ sourceId={context.source.id}
222
+ order={context.source.order}
223
+ spec={spec}
224
+ />
225
+ )
226
+ }
227
+ return null
228
+ }
229
+ return renderInlineFence(key, context, spec)
230
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * GenUI gallery: one spec exercising every white-listed node type, used as
3
+ * the canonical full-vocabulary sample (tests render it and assert every
4
+ * component family appears; docs and demos reuse it as the "show everything"
5
+ * fence).
6
+ */
7
+ import type { GenuiSpec } from './spec.ts'
8
+
9
+ /** A single spec covering all 40 node types in the vocabulary. */
10
+ export const gallerySpec: GenuiSpec = {
11
+ title: 'GenUI · 组件画廊',
12
+ gap: 14,
13
+ items: [
14
+ { type: 'text', size: 'h1', content: '排版层级' },
15
+ { type: 'text', size: 'h2', content: '二级标题' },
16
+ { type: 'text', size: 'h3', content: '三级标题' },
17
+ { type: 'text', size: 'body', content: '正文:组件白名单渲染,不经过任意 HTML 路径。' },
18
+ { type: 'text', size: 'muted', content: '弱化文本' },
19
+ { type: 'text', size: 'caption', content: '说明文字', center: true },
20
+ { type: 'row', items: [
21
+ { type: 'badge', label: '成功', tone: 'success' },
22
+ { type: 'badge', label: '警告', tone: 'warn' },
23
+ { type: 'badge', label: '危险', tone: 'danger' },
24
+ { type: 'badge', label: '强调', tone: 'accent', icon: '★' },
25
+ { type: 'avatar', name: 'Alice' },
26
+ { type: 'avatar', name: 'Bob', color: '#3d9e8f' },
27
+ { type: 'link', label: '详情链接' },
28
+ ], wrap: true },
29
+ { type: 'divider' },
30
+ { type: 'audio', src: '/demo-audio.mp3', alt: '音频播放器演示' },
31
+ { type: 'video', src: '/demo-video.mp4', alt: '视频播放器演示', poster: '/demo-video.jpg', aspectRatio: '16:9' },
32
+ { type: 'grid', cols: 3, items: [
33
+ { type: 'stat', label: 'CPU', value: '42%', delta: '+3.1%' },
34
+ { type: 'stat', label: '内存', value: '6.8 GB', delta: '-1.2%' },
35
+ { type: 'stat', label: '请求数', value: '128.4k' },
36
+ ] },
37
+ { type: 'progress', label: '训练进度', value: 72, valueLabel: '72%' },
38
+ { type: 'card', title: '性能指标', items: [
39
+ // 表头可点击排序:数值感知(千分位 / k / 万 / % 都能正确比较),
40
+ // 数值列自动右对齐。
41
+ { type: 'table', columns: ['指标', 'Q1', 'Q2', 'Q3'], rows: [
42
+ ['延迟', 92, 87, 81], ['吞吐', '1.2k', '1.4k', '1.6k'], ['错误率', '0.3%', '0.2%', '0.1%'],
43
+ ['营收', '3.5万', '4.1万', '5.2万'], ['注册', '1,234', '2,345', '3,456'],
44
+ ] },
45
+ { type: 'keyvalue', pairs: [
46
+ { key: '版本', value: 'v0.1.0' }, { key: '环境', value: 'production' }, { key: '区域', value: 'cn-east' },
47
+ ] },
48
+ ] },
49
+ { type: 'list', items: [
50
+ { title: '标题项', desc: '带描述的列表项' },
51
+ '纯文本列表项',
52
+ { title: '另一个标题' },
53
+ ] },
54
+ { type: 'chart', kind: 'bars', data: [
55
+ { label: '一', value: 10 }, { label: '二', value: 20 }, { label: '三', value: 15 },
56
+ ] },
57
+ { type: 'chart', kind: 'line', data: [
58
+ { label: '周一', value: 8 }, { label: '周二', value: 12 }, { label: '周三', value: 9 },
59
+ ] },
60
+ { type: 'chart', kind: 'donut', data: [
61
+ { label: 'A', value: 30 }, { label: 'B', value: 70 },
62
+ ] },
63
+ { type: 'chart', data: [], series: [
64
+ { label: '本月', data: [{ label: 'Q1', value: 3 }, { label: 'Q2', value: 5 }] },
65
+ { label: '上月', data: [{ label: 'Q1', value: 2 }, { label: 'Q2', value: 4 }] },
66
+ ] },
67
+ { type: 'tabs', tabs: [
68
+ { label: '概览', items: [{ type: 'text', content: '标签页一的内容' }] },
69
+ { label: '明细', items: [{ type: 'list', items: ['明细 A', '明细 B'] }] },
70
+ { label: '图表', items: [{ type: 'chart', kind: 'donut', data: [{ label: 'X', value: 40 }, { label: 'Y', value: 60 }] }] },
71
+ ] },
72
+ { type: 'col', gap: 8, items: [
73
+ { type: 'button', label: '主按钮', tone: 'primary' },
74
+ { type: 'button', label: '危险', tone: 'danger', small: true },
75
+ { type: 'button', label: '成功', tone: 'success' },
76
+ { type: 'button', label: '幽灵', tone: 'ghost', icon: '↗' },
77
+ ] },
78
+ { type: 'row', items: [
79
+ { type: 'input', label: '名称', placeholder: '输入…' },
80
+ { type: 'select', label: '环境', options: ['dev', 'staging', 'production'] },
81
+ ], wrap: true },
82
+ { type: 'row', items: [
83
+ { type: 'checkbox', label: '自动保存', checked: true },
84
+ { type: 'switch', label: '通知', checked: true },
85
+ { type: 'radio', label: '主题', options: ['浅色', '深色', '跟随系统'] },
86
+ ], wrap: true },
87
+ { type: 'textarea', label: '备注', placeholder: '多行输入…', rows: 3 },
88
+ { type: 'accordion', items: [
89
+ { title: '第一项', items: [{ type: 'json', value: { ok: true, count: 3 } }] },
90
+ { title: '第二项', items: [{ type: 'code', lang: 'ts', code: 'export const x = 1' }] },
91
+ ] },
92
+ { type: 'copy', label: '复制令牌', text: 'sk-1234567890' },
93
+ { type: 'plot', title: '波动叠加', xMin: -6.28, xMax: 6.28, series: [
94
+ { expr: 'sin(x)', label: 'sin(x)', color: '#4f8ef7' },
95
+ { expr: '0.8*cos(x)', label: 'cos', color: '#3ecf8e' },
96
+ ] },
97
+ { type: 'callout', tone: 'info', title: '提示', content: '画廊覆盖全部组件词汇。' },
98
+ { type: 'steps', current: 2, steps: [
99
+ { title: '起草', desc: '写规格' }, { title: '渲染', desc: '画组件' }, { title: '验证', desc: '跑测试' },
100
+ ] },
101
+ { type: 'diff', diffs: [
102
+ { path: 'a.ts', oldText: 'const x = 1', newText: 'const x = 2' },
103
+ ] },
104
+ { type: 'code', lang: 'json', code: '{"hello": "world"}' },
105
+ { type: 'mermaid', code: 'graph TD\nA[模型] --> B[渲染器]\nB --> C[组件]' },
106
+ { type: 'scene3d', title: '几何演示', ambient: 1, meshes: [
107
+ { shape: 'box', color: '#4f8ef7', position: [-1.4, 0, 0], rotation: [0.5, 0.8, 0] },
108
+ { shape: 'sphere', color: '#3ecf8e', position: [0, 0, 0] },
109
+ { shape: 'cone', color: '#e0a458', position: [1.4, 0, 0] },
110
+ ] },
111
+ { type: 'timeline', items: [
112
+ { title: '发布 v0.1', desc: '首个可用版本', time: '08-01' },
113
+ { title: '事件循环', desc: 'action 回流', time: '08-08' },
114
+ ] },
115
+ { type: 'file-tree', items: [
116
+ { name: 'src', type: 'dir', children: [
117
+ { name: 'client', type: 'dir', children: [{ name: 'GenuiBlock.tsx', type: 'file' }] },
118
+ { name: 'spec.ts', type: 'file' },
119
+ ] },
120
+ { name: 'README.md', type: 'file' },
121
+ ] },
122
+ { type: 'breadcrumb', items: ['首页', '组件', '画廊'] },
123
+ { type: 'quiz', question: '1 + 1 = ?', id: 'gallery-q1', options: [
124
+ { label: '1', feedback: '再想想' }, { label: '2', correct: true }, { label: '3' },
125
+ ], explanation: '二进制里 1+1=10,十进制里是 2。' },
126
+ { type: 'spacer' },
127
+ ],
128
+ }