@joplo-app/plugin-ui 0.0.0-stage → 0.4.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.
@@ -0,0 +1,1554 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { AnimatePresence } from 'motion/react';
5
+ import { motion } from '@joplo-app/kit/renderer';
6
+ import { clsx } from 'clsx';
7
+ import { twMerge } from 'tailwind-merge';
8
+ import { defineRuntime, embeddedAsset } from '@joplo-app/kit/plugin';
9
+ import { useCtx, useI18n as useHostI18n, useR, useRoot, useRootAttrs, useStroke, useLook, RenderNode, RendererContextProvider, Rough, RoughPath, roundRectPath, rectOf, nodeLabel, resolveProps, resolveValue, isSelfVisible, walkNodes, atWidth, childWidth, choicesOf, layoutRegions, styleOf } from '@joplo-app/kit/renderer';
10
+ import { ACCENT, BG, DANGER, onFill, DIMMED, FONT, INFO, INK, INK2, LINE, LINE_STRONG, MONO, OK, SOFT, SPACING, SUBTLE, SURFACE, TITLE_SIZE, WARN, color, dim, fontSize, light, radius, space, spaceNum, tint } from '@joplo-app/kit/renderer-style';
11
+ import { changeFor, dataRows, displayData, stateFlowItems } from "./story-data.js";
12
+ const useI18n = () => useHostI18n();
13
+ const flexVal = (v, d) => typeof v === 'string' && v ? v : d;
14
+ const cn = (...args) => twMerge(clsx(...args));
15
+ const VIEWPORT_SIZE = { mobile: { width: 390, height: 844 } };
16
+ export const PHONE_LANDSCAPE = { width: 772, height: 358 };
17
+ function appShellLayout(node, state, width) {
18
+ const r = layoutRegions(node, state, width);
19
+ return { header: r.header ?? null, footer: r.footer ?? null, navbar: r.navbar ?? null, aside: r.aside ?? null, padding: node.props?.padding, mobile: { navbar: !!r.navbar?.mobile, aside: !!r.aside?.mobile } };
20
+ }
21
+ /** Tabs / Radio.Group / SegmentedControl 的当前值。 */
22
+ const ValueContext = createContext(null);
23
+ /** Accordion:展开的项。 */
24
+ const AccordionContext = createContext(null);
25
+ const AccordionItemContext = createContext(null);
26
+ /** Timeline / Stepper:子项序号与当前进度。 */
27
+ const ItemProgressContext = createContext(null);
28
+ /** AppShell:各区域的尺寸与收起状态。 */
29
+ const ShellContext = createContext(null);
30
+ const str = (v, fallback = '') => (v === undefined || v === null ? fallback : typeof v === 'string' ? v : typeof v === 'number' || typeof v === 'boolean' ? String(v) : JSON.stringify(v));
31
+ const arr = (v) => (Array.isArray(v) ? v : []);
32
+ /** 可输入组件的显示值:动画预览 > 状态 inputs[id] > 绑定的 value > checked > defaultValue。 */
33
+ function useInputValue(node, p) {
34
+ const { state, preview } = useCtx();
35
+ if (preview && preview.nodeId === node.id)
36
+ return preview.text;
37
+ if (state.inputs[node.id] !== undefined)
38
+ return state.inputs[node.id];
39
+ if (p.value !== undefined)
40
+ return p.value;
41
+ if (p.checked !== undefined)
42
+ return p.checked;
43
+ return p.defaultValue ?? p.defaultChecked;
44
+ }
45
+ function textOf(node, state) {
46
+ if (node.type === '#text')
47
+ return str(resolveValue(node.props?.text, state));
48
+ return (node.children ?? []).map((c) => textOf(c, state)).join('');
49
+ }
50
+ /** 手绘描边 + 线框边框共用的盒子。 */
51
+ function boxStyle(stroke, fill) {
52
+ return { ...(stroke ? { borderColor: stroke } : {}), ...(fill ? { background: fill } : {}) };
53
+ }
54
+ // ============ 文本与 HTML ============
55
+ const HTML_TAGS = ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'main', 'section', 'header', 'footer', 'nav', 'aside', 'ul', 'ol', 'li', 'strong', 'em', 'small', 'code'];
56
+ function htmlImpl(tag) {
57
+ return function Html({ node, p, children }) {
58
+ const root = useRoot(node, p, { className: `jh-${tag}` });
59
+ const Tag = tag;
60
+ return _jsx(Tag, { ...root, children: children });
61
+ };
62
+ }
63
+ function Title({ node, p, children }) {
64
+ const order = Math.min(6, Math.max(1, Number(p.order ?? 1) || 1));
65
+ const size = fontSize(p.size) ?? `${TITLE_SIZE[order - 1]}px`;
66
+ const root = useRoot(node, p, { className: 'jt-title', style: { fontSize: size, fontWeight: order <= 2 ? 700 : 650, lineHeight: 1.3 } });
67
+ const Tag = `h${order}`;
68
+ return _jsx(Tag, { ...root, children: children });
69
+ }
70
+ function Text({ node, p, children }) {
71
+ const r = useR(p);
72
+ const clamp = typeof p.lineClamp === 'number' ? p.lineClamp : null;
73
+ const style = {
74
+ fontSize: p.inherit ? undefined : (fontSize(r('size')) ?? `${FONT.md - 1}px`),
75
+ whiteSpace: p.truncate ? 'nowrap' : 'pre-line',
76
+ lineHeight: 1.55,
77
+ ...(p.truncate ? { overflow: 'hidden', textOverflow: 'ellipsis' } : {}),
78
+ ...(clamp ? { display: '-webkit-box', WebkitLineClamp: clamp, WebkitBoxOrient: 'vertical', overflow: 'hidden' } : {}),
79
+ ...(p.c === undefined && p.color !== undefined ? { color: color(p.color) } : {}),
80
+ ...(p.gradient ? { fontWeight: 700 } : {}),
81
+ };
82
+ const root = useRoot(node, p, { style });
83
+ const inline = p.span === true || p.component === 'span' || p.inline === true;
84
+ return inline ? _jsx("span", { ...root, children: children }) : _jsx("p", { ...root, children: children });
85
+ }
86
+ function Code({ node, p, children }) {
87
+ const bg = color(p.color);
88
+ const block = p.block === true;
89
+ const root = useRoot(node, p, {
90
+ style: {
91
+ fontFamily: MONO,
92
+ fontSize: block ? 13 : '0.88em',
93
+ background: bg ? light(bg) : SUBTLE,
94
+ borderRadius: 4,
95
+ padding: block ? '10px 12px' : '1px 5px',
96
+ whiteSpace: block ? 'pre-wrap' : 'pre-wrap',
97
+ display: block ? 'block' : 'inline',
98
+ wordBreak: 'break-word',
99
+ },
100
+ });
101
+ return block ? _jsx("pre", { ...root, children: children }) : _jsx("code", { ...root, children: children });
102
+ }
103
+ function Kbd({ node, p, children }) {
104
+ const root = useRoot(node, p, { className: 'jbox', style: { display: 'inline-block', fontFamily: MONO, fontSize: 12, padding: '0 6px', borderBottomWidth: 3, borderColor: LINE_STRONG, borderRadius: 4, background: SUBTLE } });
105
+ return (_jsx("kbd", { ...root, "data-soft": "", children: children }));
106
+ }
107
+ function Blockquote({ node, p, children }) {
108
+ const c = color(p.color) ?? INFO;
109
+ const root = useRoot(node, p, { style: { borderLeft: `4px solid ${c}`, background: light(c), padding: '12px 16px', borderRadius: 4 } });
110
+ return (_jsxs("blockquote", { ...root, children: [_jsx("div", { style: { whiteSpace: 'pre-line' }, children: children }), p.cite ? _jsxs("cite", { style: { display: 'block', marginTop: 6, fontSize: 13, color: DIMMED, fontStyle: 'normal' }, children: ["\u2014 ", str(p.cite)] }) : null] }));
111
+ }
112
+ function List({ node, p, children }) {
113
+ const ordered = p.type === 'ordered';
114
+ const root = useRoot(node, p, { style: { listStyle: ordered ? 'decimal' : 'disc', paddingLeft: 22, display: 'flex', flexDirection: 'column', gap: space(p.spacing) ?? '4px', fontSize: fontSize(p.size) } });
115
+ return ordered ? _jsx("ol", { ...root, children: children }) : _jsx("ul", { ...root, children: children });
116
+ }
117
+ function ListItem({ node, p, children }) {
118
+ const root = useRoot(node, p, { style: { whiteSpace: 'pre-line' } });
119
+ return _jsx("li", { ...root, children: children });
120
+ }
121
+ function Anchor({ node, p, disabled, children }) {
122
+ const c = color(p.c) ?? INFO;
123
+ const root = useRoot(node, p, { disabled, className: 'cursor-pointer', style: { color: disabled ? DIMMED : c, textDecoration: p.underline === 'never' ? 'none' : 'underline', textUnderlineOffset: 4, fontSize: fontSize(p.size) } });
124
+ return (_jsx("a", { ...root, role: "link", tabIndex: 0, children: children }));
125
+ }
126
+ // ============ 布局 ============
127
+ function Box({ node, p, children }) {
128
+ const root = useRoot(node, p);
129
+ return _jsx("div", { ...root, children: children });
130
+ }
131
+ function Space({ node, p }) {
132
+ const r = useR(p);
133
+ const root = useRoot(node, p, { style: { height: space(r('h')), width: space(r('w')), flexShrink: 0 } });
134
+ return _jsx("div", { ...root, "aria-hidden": true });
135
+ }
136
+ function Stack({ node, p, children }) {
137
+ const r = useR(p);
138
+ const root = useRoot(node, p, {
139
+ style: { display: 'flex', flexDirection: 'column', gap: space(r('gap')) ?? `${SPACING.md}px`, alignItems: flexVal(r('align'), 'stretch'), justifyContent: flexVal(r('justify'), 'flex-start') },
140
+ });
141
+ return _jsx("div", { ...root, children: children });
142
+ }
143
+ function Group({ node, p, children }) {
144
+ const r = useR(p);
145
+ const root = useRoot(node, p, {
146
+ className: r('grow') ? 'jgrow' : undefined,
147
+ style: {
148
+ display: 'flex',
149
+ flexDirection: 'row',
150
+ gap: space(r('gap')) ?? `${SPACING.md}px`,
151
+ alignItems: flexVal(r('align'), 'center'),
152
+ justifyContent: flexVal(r('justify'), 'flex-start'),
153
+ flexWrap: flexVal(r('wrap'), 'wrap') ?? 'wrap',
154
+ },
155
+ });
156
+ return _jsx("div", { ...root, children: children });
157
+ }
158
+ function Flex({ node, p, children }) {
159
+ const r = useR(p);
160
+ const root = useRoot(node, p, {
161
+ style: {
162
+ display: p.inline ? 'inline-flex' : 'flex',
163
+ flexDirection: flexVal(r('direction'), 'row') ?? 'row',
164
+ flexWrap: flexVal(r('wrap'), 'nowrap') ?? 'nowrap',
165
+ gap: space(r('gap')),
166
+ rowGap: space(r('rowGap')),
167
+ columnGap: space(r('columnGap')),
168
+ alignItems: flexVal(r('align')),
169
+ justifyContent: flexVal(r('justify')),
170
+ },
171
+ });
172
+ return _jsx("div", { ...root, children: children });
173
+ }
174
+ function SimpleGrid({ node, p, children }) {
175
+ const r = useR(p);
176
+ const cols = Math.max(1, Number(r('cols') ?? 1) || 1);
177
+ const gap = space(r('spacing')) ?? `${SPACING.md}px`;
178
+ const root = useRoot(node, p, { style: { display: 'grid', gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, columnGap: gap, rowGap: space(r('verticalSpacing')) ?? gap } });
179
+ return (_jsx("div", { ...root, "data-cols": cols, children: children }));
180
+ }
181
+ const GridContext = createContext({ columns: 12, gutter: 16, grow: false });
182
+ function Grid({ node, p, children }) {
183
+ const r = useR(p);
184
+ const columns = Math.max(1, Number(r('columns') ?? 12) || 12);
185
+ const gutter = spaceNum(r('gutter'), SPACING.md);
186
+ const root = useRoot(node, p, { style: { display: 'flex', flexWrap: 'wrap', gap: gutter, alignItems: flexVal(r('align'), 'stretch'), justifyContent: flexVal(r('justify'), 'flex-start') } });
187
+ return (_jsx(GridContext.Provider, { value: { columns, gutter, grow: p.grow === true }, children: _jsx("div", { ...root, children: children }) }));
188
+ }
189
+ function GridCol({ node, p, children }) {
190
+ const r = useR(p);
191
+ const g = useContext(GridContext);
192
+ const span = r('span') ?? 12;
193
+ const offset = Number(r('offset') ?? 0) || 0;
194
+ const frac = (n) => `calc((100% + ${g.gutter}px) * ${n / g.columns} - ${g.gutter}px)`;
195
+ const style = span === 'auto' ? { flex: '1 1 0', minWidth: 0 } : span === 'content' ? { flex: '0 0 auto' } : { flex: g.grow ? `1 0 ${frac(Number(span) || 12)}` : `0 0 ${frac(Number(span) || 12)}`, maxWidth: g.grow ? undefined : frac(Number(span) || 12), minWidth: 0 };
196
+ if (offset)
197
+ style.marginLeft = `calc((100% + ${g.gutter}px) * ${offset / g.columns})`;
198
+ const root = useRoot(node, p, { style });
199
+ return _jsx("div", { ...root, children: children });
200
+ }
201
+ const CONTAINER = { xs: 540, sm: 720, md: 960, lg: 1140, xl: 1320 };
202
+ function Container({ node, p, children }) {
203
+ const r = useR(p);
204
+ const size = r('size');
205
+ const max = p.fluid ? '100%' : typeof size === 'number' ? `${size}px` : `${CONTAINER[str(size, 'md')] ?? 960}px`;
206
+ const root = useRoot(node, p, { style: { maxWidth: max, marginLeft: 'auto', marginRight: 'auto', paddingLeft: SPACING.md, paddingRight: SPACING.md, width: '100%' } });
207
+ return _jsx("div", { ...root, children: children });
208
+ }
209
+ function Center({ node, p, children }) {
210
+ const root = useRoot(node, p, { style: { display: p.inline ? 'inline-flex' : 'flex', alignItems: 'center', justifyContent: 'center' } });
211
+ return _jsx("div", { ...root, children: children });
212
+ }
213
+ function Paper({ node, p, children }) {
214
+ const mode = useStroke();
215
+ const border = p.withBorder === true;
216
+ const shadow = typeof p.shadow === 'string' && p.shadow !== 'none';
217
+ const root = useRoot(node, p, {
218
+ className: border ? 'jbox' : undefined,
219
+ style: { background: SURFACE, borderRadius: p.radius !== undefined ? radius(p.radius) : 'var(--jp-radius)', ...(shadow ? { boxShadow: 'var(--jp-shadow)' } : {}), position: 'relative' },
220
+ });
221
+ return (_jsxs("div", { ...root, "data-soft": "", children: [border ? _jsx(Rough, { seedKey: node.id, stroke: SOFT }) : null, children] }));
222
+ }
223
+ function Card({ node, p, children }) {
224
+ const r = useR(p);
225
+ const pad = spaceNum(r('padding'), SPACING.md);
226
+ const border = p.withBorder === true;
227
+ const root = useRoot(node, p, {
228
+ className: 'jbox jcard [.jstage[data-stroke=clean]_&]:rounded-lg',
229
+ style: { display: 'flex', flexDirection: 'column', background: SURFACE, padding: pad, ['--jcard-p']: `${pad}px` },
230
+ });
231
+ return (_jsxs("div", { ...root, "data-soft": border ? undefined : '', children: [_jsx(Rough, { seedKey: node.id, stroke: border ? INK : SOFT }), children] }));
232
+ }
233
+ function CardSection({ node, p, children }) {
234
+ const border = p.withBorder === true;
235
+ const root = useRoot(node, p, {
236
+ className: 'jcard-sec',
237
+ style: { marginLeft: 'calc(-1 * var(--jcard-p, 16px))', marginRight: 'calc(-1 * var(--jcard-p, 16px))', ...(p.inheritPadding ? { paddingLeft: 'var(--jcard-p, 16px)', paddingRight: 'var(--jcard-p, 16px)' } : {}), ...(border ? { borderTop: '1px solid #dee2e6', borderBottom: '1px solid #dee2e6' } : {}) },
238
+ });
239
+ return _jsx("div", { ...root, children: children });
240
+ }
241
+ function Divider({ node, p }) {
242
+ const vertical = p.orientation === 'vertical';
243
+ const label = p.label !== undefined ? str(p.label) : '';
244
+ const pos = str(p.labelPosition, 'left');
245
+ const c = color(p.color) ?? LINE;
246
+ const root = useRoot(node, p, { style: vertical ? { alignSelf: 'stretch', width: 1, background: c, flexShrink: 0 } : { display: 'flex', alignItems: 'center', gap: 8, width: '100%', color: DIMMED, fontSize: 13 } });
247
+ if (vertical)
248
+ return _jsx("div", { ...root, role: "separator", "aria-orientation": "vertical" });
249
+ const line = _jsx("span", { style: { flex: 1, height: 1, background: c } });
250
+ return (_jsxs("div", { ...root, role: "separator", children: [label && pos !== 'left' ? line : null, label ? _jsx("span", { children: label }) : null, !label || pos !== 'right' ? line : null] }));
251
+ }
252
+ function ScrollArea({ node, p, children }) {
253
+ const root = useRoot(node, p, { style: { position: 'relative', overflow: 'auto' } });
254
+ return _jsx("div", { ...root, children: children });
255
+ }
256
+ // ============ AppShell ============
257
+ function AppShell({ node, p, children }) {
258
+ const { state, width } = useCtx();
259
+ const layout = appShellLayout(node, state, width);
260
+ let hasBurger = false;
261
+ walkNodes(node, (n) => {
262
+ if (n.type === 'ui:Burger')
263
+ hasBurger = true;
264
+ });
265
+ const navW = layout.navbar && !layout.navbar.collapsed && !layout.mobile.navbar ? layout.navbar.size : 0;
266
+ const asideW = layout.aside && !layout.aside.collapsed && !layout.mobile.aside ? layout.aside.size : 0;
267
+ const headH = layout.header && !layout.header.collapsed ? layout.header.size : 0;
268
+ const footH = layout.footer && !layout.footer.collapsed ? layout.footer.size : 0;
269
+ const root = useRoot(node, p, {
270
+ style: {
271
+ display: 'grid',
272
+ height: '100%',
273
+ gridTemplateColumns: `${navW}px minmax(0, 1fr) ${asideW}px`,
274
+ gridTemplateRows: `${headH}px minmax(0, 1fr) ${footH}px`,
275
+ gridTemplateAreas: '"h h h" "n m a" "f f f"',
276
+ position: 'relative',
277
+ },
278
+ });
279
+ return (_jsx(ShellContext.Provider, { value: { layout, autoBurger: !hasBurger && (layout.mobile.navbar || false) && !!layout.navbar }, children: _jsx("div", { ...root, "data-shell-mobile": layout.mobile.navbar ? '' : undefined, children: children }) }));
280
+ }
281
+ function ShellHeader({ node, p, children }) {
282
+ const shell = useContext(ShellContext);
283
+ const root = useRoot(node, p, { style: { gridArea: 'h', borderBottom: `1px solid ${LINE}`, background: SURFACE, minWidth: 0, position: 'relative', zIndex: 2 } });
284
+ if (shell?.layout.header?.collapsed)
285
+ return null;
286
+ return (_jsxs("header", { ...root, children: [shell?.autoBurger ? (_jsx("span", { "aria-hidden": true, "data-jauto-burger": "", style: { position: 'absolute', left: 14, top: '50%', transform: 'translateY(-50%)' }, children: _jsx(BurgerIcon, { opened: false, size: 18 }) })) : null, _jsx("div", { style: { height: '100%', paddingLeft: shell?.autoBurger ? 40 : 0 }, children: children })] }));
287
+ }
288
+ function ShellSide({ node, p, children }) {
289
+ const shell = useContext(ShellContext);
290
+ const nav = node.type === 'ui:AppShell.Navbar';
291
+ const region = nav ? shell?.layout.navbar : shell?.layout.aside;
292
+ const mobile = nav ? shell?.layout.mobile.navbar : shell?.layout.mobile.aside;
293
+ const overlay = !!mobile && region && !region.collapsed;
294
+ const root = useRoot(node, p, {
295
+ style: overlay
296
+ ? { position: 'absolute', left: 0, right: 0, top: shell.layout.header?.size ?? 0, bottom: 0, zIndex: 5, background: SURFACE, overflow: 'auto', display: 'flex', flexDirection: 'column' }
297
+ : { gridArea: nav ? 'n' : 'a', background: SUBTLE, minHeight: 0, overflow: 'auto', display: 'flex', flexDirection: 'column', [nav ? 'borderRight' : 'borderLeft']: `1px solid ${LINE}` },
298
+ });
299
+ if (region?.collapsed)
300
+ return null;
301
+ return _jsx("nav", { ...root, children: children });
302
+ }
303
+ function ShellMain({ node, p, children }) {
304
+ const shell = useContext(ShellContext);
305
+ const { width, state } = useCtx();
306
+ const pad = space(atWidth(resolveValue(shell?.layout.padding, state), width)) ?? '0px';
307
+ const root = useRoot(node, p, { style: { gridArea: 'm', minHeight: 0, minWidth: 0, overflow: 'auto', padding: pad } });
308
+ return _jsx("main", { ...root, children: children });
309
+ }
310
+ function ShellFooter({ node, p, children }) {
311
+ const shell = useContext(ShellContext);
312
+ const root = useRoot(node, p, { style: { gridArea: 'f', borderTop: `1px solid ${LINE}`, background: SURFACE, minWidth: 0 } });
313
+ if (shell?.layout.footer?.collapsed)
314
+ return null;
315
+ return _jsx("footer", { ...root, children: children });
316
+ }
317
+ function BurgerIcon({ opened, size = 18 }) {
318
+ const bar = (extra) => ({ position: 'absolute', left: 0, width: size, height: 2, background: INK, borderRadius: 1, transition: 'transform .2s', ...extra });
319
+ return (_jsxs("span", { style: { position: 'relative', display: 'inline-block', width: size, height: size * 0.75 }, children: [_jsx("span", { style: bar(opened ? { top: '50%', transform: 'rotate(45deg)' } : { top: 0 }) }), opened ? null : _jsx("span", { style: bar({ top: '50%', marginTop: -1 }) }), _jsx("span", { style: bar(opened ? { top: '50%', transform: 'rotate(-45deg)' } : { bottom: 0 }) })] }));
320
+ }
321
+ function Burger({ node, p, disabled }) {
322
+ const { t: i18n } = useI18n();
323
+ const root = useRoot(node, p, { disabled, style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, flexShrink: 0 } });
324
+ return (_jsx("button", { ...root, type: "button", "aria-label": str(p['aria-label'], i18n.stage.navigation), "aria-expanded": p.opened === true, children: _jsx(BurgerIcon, { opened: p.opened === true, size: p.size === 'sm' ? 16 : p.size === 'lg' ? 22 : 18 }) }));
325
+ }
326
+ // ============ 展示 ============
327
+ const BADGE_H = { xs: 16, sm: 18, md: 20, lg: 26, xl: 32 };
328
+ function Badge({ node, p, children }) {
329
+ const mode = useStroke();
330
+ const variant = str(p.variant, 'filled');
331
+ const base = color(p.color) ?? INK;
332
+ const t = variant === 'outline' ? { stroke: base, text: base }
333
+ : variant === 'light' ? { stroke: light(base), fill: light(base), text: base }
334
+ : variant === 'dot' || variant === 'default' ? { stroke: LINE_STRONG, text: INK }
335
+ : variant === 'transparent' ? { stroke: 'transparent', text: base }
336
+ : variant === 'white' ? { stroke: SURFACE, fill: SURFACE, text: base }
337
+ : { stroke: base, fill: base, text: onFill(base) };
338
+ const h = BADGE_H[str(p.size, 'md')] ?? 20;
339
+ const root = useRoot(node, p, {
340
+ className: 'jbox',
341
+ style: { display: 'inline-flex', alignItems: 'center', gap: 4, height: h + 4, padding: '0 10px', borderRadius: 999, fontSize: Math.max(11, h - 7), color: t.text, ...(mode === 'clean' ? boxStyle(t.stroke, t.fill) : {}), whiteSpace: 'nowrap', width: p.fullWidth ? '100%' : 'fit-content', justifyContent: 'center' },
342
+ });
343
+ return (_jsxs("span", { ...root, children: [_jsx(Rough, { seedKey: node.id, shape: "ellipse", stroke: t.stroke, fill: t.fill, fillStyle: "solid", strokeWidth: 1, inset: 1 }), variant === 'dot' ? _jsx("span", { style: { width: 6, height: 6, borderRadius: 3, background: base } }) : null, _jsx("span", { className: "jtrunc", "data-jtext": "", children: children })] }));
344
+ }
345
+ const AVATAR = { xs: 16, sm: 26, md: 38, lg: 56, xl: 84 };
346
+ function initials(name) {
347
+ const t = name.trim();
348
+ if (!t)
349
+ return '?';
350
+ if (/^[\x00-\x7f]+$/.test(t))
351
+ return t.split(/\s+/).slice(0, 2).map((w) => w[0].toUpperCase()).join('');
352
+ return t.slice(0, 1);
353
+ }
354
+ function Avatar({ node, p, children }) {
355
+ const { state } = useCtx();
356
+ const size = typeof p.size === 'number' ? p.size : (AVATAR[str(p.size, 'md')] ?? 38);
357
+ const c = color(p.color) ?? INK2;
358
+ const name = str(p.name ?? p.alt) || textOf(node, state);
359
+ const root = useRoot(node, p, { className: 'jbox', style: { width: size, height: size, borderRadius: 999, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: Math.max(10, size * 0.4), color: c, background: light(c), flexShrink: 0, fontWeight: 600 } });
360
+ return (_jsxs("span", { ...root, "data-jround": "", children: [_jsx(Rough, { seedKey: node.id, shape: "ellipse", stroke: c, strokeWidth: 1.1 }), _jsx("span", { children: p.name || p.alt || !children ? initials(name) : children })] }));
361
+ }
362
+ /** 内嵌的图片(asset() 生成的 data:image/… URI)才真正画出来;网址、路径不去加载(编译期会提示),仍画占位。 */
363
+ /** 嵌入的图片:asset() 的 data URL,或发布产物里的素材文件(D98);远程地址仍画占位。 */
364
+ export const embeddedImage = (src) => embeddedAsset(src, 'image');
365
+ function ImageBox({ node, p }) {
366
+ const { t: i18n } = useI18n();
367
+ const r = useR(p);
368
+ const src = embeddedImage(p.src);
369
+ if (src) {
370
+ const h = dim(r('h'));
371
+ const fit = p.fit === 'contain' ? 'contain' : 'cover';
372
+ const root = useRoot(node, p, { style: { display: 'block', position: 'relative', overflow: 'hidden', width: dim(r('w')) ?? '100%', height: h ?? 'auto', borderRadius: radius(p.radius) } });
373
+ return (_jsx("div", { ...root, role: "img", "aria-label": str(p.alt, i18n.stage.image), "data-jimage": "", children: _jsx("img", { src: src, alt: "", draggable: false, style: { display: 'block', width: '100%', height: h ? '100%' : 'auto', objectFit: fit, objectPosition: str(p.position, 'center') } }) }));
374
+ }
375
+ const root = useRoot(node, p, { className: 'jbox', style: { display: 'flex', alignItems: 'center', justifyContent: 'center', width: dim(r('w')) ?? '100%', height: dim(r('h')) ?? 160, background: SUBTLE, color: DIMMED, fontSize: 13, borderRadius: radius(p.radius) } });
376
+ return (_jsxs("div", { ...root, role: "img", "aria-label": str(p.alt, i18n.stage.image), "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT, fill: LINE, fillStyle: "cross-hatch" }), _jsxs("span", { style: { background: 'rgba(255,255,255,.9)', padding: '0 8px', borderRadius: 4 }, children: ["\uD83D\uDDBC ", str(p.alt, i18n.stage.image)] })] }));
377
+ }
378
+ function ThemeIcon({ node, p, children }) {
379
+ const size = typeof p.size === 'number' ? p.size : (AVATAR[str(p.size, 'md')] ?? 28) * 0.8;
380
+ const c = color(p.color) ?? INK;
381
+ const filled = str(p.variant, 'filled') === 'filled';
382
+ const root = useRoot(node, p, { className: 'jbox', style: { width: size, height: size, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: radius(p.radius ?? 'sm'), background: filled ? c : light(c), color: filled ? onFill(c) : c, flexShrink: 0, fontSize: size * 0.55 } });
383
+ return (_jsx("span", { ...root, children: _jsx("span", { children: children }) }));
384
+ }
385
+ function Indicator({ node, p, disabled, children }) {
386
+ const c = color(p.color) ?? DANGER;
387
+ const label = p.label !== undefined ? str(p.label) : '';
388
+ const root = useRoot(node, p, { style: { position: 'relative', display: p.inline ? 'inline-block' : 'block', width: 'fit-content' } });
389
+ return (_jsxs("div", { ...root, children: [children, disabled ? null : (_jsx("span", { "data-jindicator": "", style: { position: 'absolute', top: -4, right: -4, minWidth: label ? 18 : 10, height: label ? 18 : 10, padding: label ? '0 5px' : 0, borderRadius: 999, background: c, color: '#fff', fontSize: 11, lineHeight: '18px', textAlign: 'center', border: '2px solid #fff', zIndex: 2 }, children: label }))] }));
390
+ }
391
+ function Timeline({ node, p, children }) {
392
+ const active = Number(resolveValue(p.active, useCtx().state) ?? -1);
393
+ const ids = (node.children ?? []).filter((c) => c.type === 'ui:Timeline.Item').map((c) => c.id);
394
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column' } });
395
+ return (_jsx(ItemProgressContext.Provider, { value: { ids, active: Number.isFinite(active) ? active : -1, content: null }, children: _jsx("div", { ...root, children: children }) }));
396
+ }
397
+ function TimelineItem({ node, p, children }) {
398
+ const seq = useContext(ItemProgressContext);
399
+ const i = seq ? seq.ids.indexOf(node.id) : 0;
400
+ const done = !!seq && i <= seq.active;
401
+ const last = !!seq && i === seq.ids.length - 1;
402
+ const c = color(p.color) ?? INK;
403
+ const root = useRoot(node, p, { style: { position: 'relative', paddingLeft: 30, paddingBottom: last ? 0 : 18 } });
404
+ return (_jsxs("div", { ...root, "data-active": done ? '' : undefined, children: [last ? null : _jsx("span", { "aria-hidden": true, style: { position: 'absolute', left: 9, top: 20, bottom: 0, width: 2, background: done && seq && i < seq.active ? c : LINE } }), _jsx("span", { "aria-hidden": true, style: { position: 'absolute', left: 0, top: 2, width: 20, height: 20, borderRadius: 10, border: `2px solid ${done ? c : LINE_STRONG}`, background: done ? c : SURFACE, color: '#fff', fontSize: 11, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: p.bullet !== undefined ? str(p.bullet) : null }), p.title !== undefined ? _jsx("div", { style: { fontWeight: 600, lineHeight: 1.5 }, children: str(p.title) }) : null, _jsx("div", { style: { color: INK2, fontSize: 14, whiteSpace: 'pre-line' }, children: children })] }));
405
+ }
406
+ // ============ Table / DataTable ============
407
+ const cellPad = '8px 12px';
408
+ function TableShell({ node, p, children }) {
409
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { width: '100%', background: SURFACE, overflowX: 'auto' } });
410
+ return (_jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: INK2, strokeWidth: 1.1 }), children] }));
411
+ }
412
+ function Table({ node, p, children }) {
413
+ const data = p.data && typeof p.data === 'object' && !Array.isArray(p.data) ? p.data : null;
414
+ const striped = p.striped === true;
415
+ return (_jsx(TableShell, { node: node, p: p, disabled: false, children: _jsx("table", { className: cn('jtable w-full border-collapse text-[14px]', striped && 'jtable-striped'), children: data ? (_jsxs(_Fragment, { children: [data.caption ? _jsx("caption", { style: { captionSide: 'bottom', color: DIMMED, fontSize: 13, padding: 6 }, children: str(data.caption) }) : null, Array.isArray(data.head) ? (_jsx("thead", { children: _jsx("tr", { children: data.head.map((h, i) => (_jsx("th", { children: str(h) }, i))) }) })) : null, _jsx("tbody", { children: arr(data.body).map((row, i) => (_jsx("tr", { children: arr(row).map((c, j) => (_jsx("td", { children: str(c) }, j))) }, i))) })] })) : (children) }) }));
416
+ }
417
+ const tablePart = (Tag) => function TablePart({ node, p, children }) {
418
+ const root = useRoot(node, p);
419
+ return _jsx(Tag, { ...root, children: children });
420
+ };
421
+ function getPath(o, path) {
422
+ let cur = o;
423
+ for (const k of path.split('.')) {
424
+ if (cur === null || typeof cur !== 'object')
425
+ return undefined;
426
+ cur = cur[k];
427
+ }
428
+ return cur;
429
+ }
430
+ const humanize = (s) => s.replace(/[_.-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
431
+ /** 选中的行:selectedRecords 里的 id(或带 idAccessor 的记录)。 */
432
+ export function selectedIds(selected, idAccessor) {
433
+ return new Set(arr(selected).map((x) => (x && typeof x === 'object' ? str(getPath(x, idAccessor)) : str(x))).filter(Boolean));
434
+ }
435
+ /**
436
+ * DataTable(mantine-datatable):records(可绑定)+ columns [{ accessor, title, textAlign, width, sortable, badge, avatar }] + noRecordsText。
437
+ * D68:sortStatus(表头箭头,只是显示,排好序的 records 由步骤 set 进去)、selectedRecords + idAccessor(勾选列、选中行底色)、
438
+ * fetching(加载遮罩)。单元格仍然只是文字,badge / avatar 让最常见的两种单元格不必改写成 Table.*。
439
+ */
440
+ function DataTable({ node, p }) {
441
+ const { t: i18n } = useI18n();
442
+ const cols = arr(p.columns)
443
+ .filter((c) => !!c && typeof c === 'object' && c.hidden !== true)
444
+ .map((c, i) => {
445
+ const accessor = str(c.accessor ?? c.accessorKey ?? `col${i}`);
446
+ const badge = c.badge === true ? true : c.badge && typeof c.badge === 'object' && !Array.isArray(c.badge) ? c.badge : null;
447
+ return { accessor, title: c.title !== undefined ? str(c.title) : c.header !== undefined ? str(c.header) : humanize(accessor), align: str(c.textAlign, 'left'), width: dim(c.width), sortable: c.sortable === true, badge, avatar: c.avatar === true };
448
+ });
449
+ const rows = arr(p.records ?? p.data);
450
+ const sort = p.sortStatus && typeof p.sortStatus === 'object' ? p.sortStatus : null;
451
+ const sortCol = sort ? str(sort.columnAccessor) : null;
452
+ const sortDir = sort && str(sort.direction) === 'desc' ? 'desc' : 'asc';
453
+ const idAccessor = str(p.idAccessor, 'id');
454
+ const selectable = p.selectedRecords !== undefined;
455
+ const selected = selectedIds(p.selectedRecords, idAccessor);
456
+ const allOn = selectable && rows.length > 0 && rows.every((r) => selected.has(r && typeof r === 'object' ? str(getPath(r, idAccessor)) : str(r)));
457
+ const someOn = selectable && !allOn && selected.size > 0;
458
+ const fetching = p.fetching === true;
459
+ // 勾选框不用 jbox(手绘描边会把边框交给 Rough),未勾选时也要看得见
460
+ const box = (on, mixed = false) => (_jsx("span", { "aria-hidden": true, className: "inline-flex items-center justify-center", style: { width: 16, height: 16, borderRadius: 3, border: `1.5px solid ${on || mixed ? INK : INK2}`, background: on || mixed ? INK : SURFACE, color: BG, fontSize: 11, lineHeight: 1, boxSizing: 'border-box' }, "data-jcheck": on ? 'on' : mixed ? 'mixed' : 'off', children: on ? '✓' : mixed ? '−' : '' }));
461
+ const cell = (c, r) => {
462
+ const v = r && typeof r === 'object' ? str(getPath(r, c.accessor)) : str(r);
463
+ if (c.badge && v) {
464
+ const tone = c.badge === true ? undefined : color(c.badge[v]);
465
+ const base = tone ?? INK2;
466
+ return _jsx("span", { style: { display: 'inline-flex', alignItems: 'center', height: 22, padding: '0 9px', borderRadius: 999, fontSize: 12, color: base, background: light(base), whiteSpace: 'nowrap' }, "data-jbadge": v, children: v });
467
+ }
468
+ if (c.avatar && v)
469
+ return (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { "aria-hidden": true, style: { width: 26, height: 26, borderRadius: 999, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 600, color: INK2, background: light(INK2), flexShrink: 0 }, children: initials(v) }), _jsx("span", { children: v })] }));
470
+ return v;
471
+ };
472
+ return (_jsx(TableShell, { node: node, p: p, disabled: false, children: _jsxs("div", { style: { position: 'relative' }, "data-jfetching": fetching ? '' : undefined, children: [_jsxs("table", { className: cn('jtable w-full border-collapse text-[14px]', p.striped === true && 'jtable-striped'), children: [_jsx("thead", { children: _jsxs("tr", { children: [selectable ? _jsx("th", { style: { width: 36 }, children: box(allOn, someOn) }) : null, cols.map((c) => {
473
+ const sorted = sortCol === c.accessor;
474
+ return (_jsxs("th", { style: { textAlign: c.align, width: c.width }, "aria-sort": sorted ? (sortDir === 'desc' ? 'descending' : 'ascending') : undefined, "data-jsort": sorted ? sortDir : undefined, children: [c.title, sorted ? _jsx("span", { "aria-hidden": true, style: { marginLeft: 4 }, children: sortDir === 'desc' ? '▼' : '▲' }) : c.sortable ? _jsx("span", { "aria-hidden": true, style: { marginLeft: 4, color: SOFT }, children: "\u21C5" }) : null] }, c.accessor));
475
+ })] }) }), _jsx("tbody", { children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: Math.max(cols.length + (selectable ? 1 : 0), 1), style: { padding: '18px 12px', textAlign: 'center', color: DIMMED }, "data-jempty": "", children: str(p.noRecordsText ?? p.empty, i18n.stage.noRecords) }) })) : (rows.map((r, i) => {
476
+ const on = selectable && selected.has(r && typeof r === 'object' ? str(getPath(r, idAccessor)) : str(r));
477
+ return (_jsxs("tr", { "data-jrow-index": i, "data-jselected-row": on ? '' : undefined, "aria-selected": selectable ? on : undefined, children: [selectable ? _jsx("td", { children: box(on) }) : null, cols.map((c) => (_jsx("td", { style: { textAlign: c.align }, children: cell(c, r) }, c.accessor)))] }, i));
478
+ })) })] }), fetching ? (_jsx("div", { style: { position: 'absolute', inset: 2, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'color-mix(in srgb, var(--jp-surface) 65%, transparent)' }, role: "status", children: _jsx(Spinner, { size: 24, c: INK }) })) : null] }) }));
479
+ }
480
+ // ============ Accordion ============
481
+ function Accordion({ node, p, children }) {
482
+ const v = useInputValue(node, p);
483
+ const open = new Set(Array.isArray(v) ? v.map(String) : v !== undefined && v !== null && v !== '' ? [str(v)] : []);
484
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { display: 'flex', flexDirection: 'column', background: SURFACE } });
485
+ return (_jsx(AccordionContext.Provider, { value: { owner: node.id, open }, children: _jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT }), children] }) }));
486
+ }
487
+ function AccordionItem({ node, p, children }) {
488
+ const acc = useContext(AccordionContext);
489
+ const value = str(p.value);
490
+ const root = useRoot(node, p, { className: 'jacc-item', style: { borderBottom: `1px solid ${LINE}` } });
491
+ return (_jsx(AccordionItemContext.Provider, { value: { value, open: !!acc?.open.has(value) }, children: _jsx("div", { ...root, children: children }) }));
492
+ }
493
+ function AccordionControl({ node, p, disabled, children }) {
494
+ const item = useContext(AccordionItemContext);
495
+ const owner = useContext(AccordionContext);
496
+ const ctx = useCtx();
497
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', alignItems: 'center', width: '100%', padding: '10px 14px', gap: 8, fontWeight: 500, textAlign: 'left', color: disabled ? DIMMED : undefined } });
498
+ return (_jsxs("button", { ...root, type: "button", "aria-expanded": !!item?.open, onClick: ctx.interactive ? e => { e.stopPropagation(); if (!disabled && owner && item)
499
+ ctx.act?.(node.id, { type: 'click' }, { type: 'select', target: owner.owner, value: item.value }); } : undefined, children: [_jsx("span", { className: "jtrunc", style: { flex: 1 }, "data-jtext": "", children: children }), _jsx("span", { "aria-hidden": true, style: { transform: item?.open ? 'rotate(180deg)' : undefined, color: DIMMED }, children: "\u25BE" })] }));
500
+ }
501
+ function AccordionPanel({ node, p, children }) {
502
+ const item = useContext(AccordionItemContext);
503
+ const root = useRoot(node, p, { style: { padding: '0 14px 12px', whiteSpace: 'pre-line', fontSize: 14, color: INK2 } });
504
+ if (item && !item.open)
505
+ return null;
506
+ return _jsx("div", { ...root, children: children });
507
+ }
508
+ // ============ Alert / Notification / Loader / Progress ============
509
+ function Alert({ node, p, children }) {
510
+ const mode = useStroke();
511
+ const c = color(p.color) ?? INFO;
512
+ const variant = str(p.variant, 'light');
513
+ const filled = variant === 'filled';
514
+ const danger = p.color === 'red';
515
+ const root = useRoot(node, p, {
516
+ className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md',
517
+ style: { display: 'flex', alignItems: 'flex-start', gap: 10, width: '100%', padding: '12px 16px', background: filled ? c : variant === 'outline' || variant === 'white' ? SURFACE : light(c), color: filled ? onFill(c) : INK, ...(mode === 'clean' ? { borderColor: c } : {}) },
518
+ });
519
+ return (_jsxs("div", { ...root, role: danger ? 'alert' : 'status', children: [_jsx(Rough, { seedKey: node.id, stroke: c, fill: filled ? c : undefined, fillStyle: "solid" }), _jsx("span", { "aria-hidden": true, style: { width: 20, height: 20, borderRadius: 10, flexShrink: 0, marginTop: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, background: filled ? '#fff' : c, color: filled ? c : '#fff' }, children: p.icon !== undefined ? str(p.icon) : danger ? '!' : 'i' }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0, flex: 1 }, children: [p.title !== undefined ? _jsx("div", { style: { fontWeight: 700, color: filled ? onFill(c) : c, lineHeight: 1.4 }, children: str(p.title) }) : null, _jsx("div", { style: { fontSize: 14, lineHeight: 1.5, whiteSpace: 'pre-line' }, children: children })] }), p.withCloseButton ? (_jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: "\u00D7" })) : null] }));
520
+ }
521
+ /** Notification 的浮动位置(D68):没写 position 时内联;写了时浮在当前页面(弹窗层)的那个角或那条边上。 */
522
+ export const NOTIFICATION_POSITIONS = ['top', 'top-left', 'top-right', 'bottom', 'bottom-left', 'bottom-right'];
523
+ export function notificationPlacement(position) {
524
+ if (typeof position !== 'string' || !NOTIFICATION_POSITIONS.includes(position))
525
+ return null;
526
+ const [v, hpos] = position.split('-');
527
+ return { position: 'absolute', [v]: 16, ...(hpos ? { [hpos]: 16 } : { left: '50%', transform: 'translateX(-50%)' }), width: 'min(380px, calc(100% - 32px))', zIndex: 6 };
528
+ }
529
+ function Notification({ node, p, children }) {
530
+ const { dialogLayer, animate } = useCtx();
531
+ const c = color(p.color) ?? INFO;
532
+ const place = notificationPlacement(p.position);
533
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 16px 12px 20px', background: SURFACE, boxShadow: place ? 'var(--jp-shadow)' : '3px 3px 0 rgba(30,30,30,.1)', position: 'relative' } });
534
+ const body = (_jsxs("div", { ...root, role: "status", "data-soft": "", "data-jfloating": place ? str(p.position) : undefined, children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT }), _jsx("span", { "aria-hidden": true, style: { position: 'absolute', left: 6, top: 10, bottom: 10, width: 4, borderRadius: 2, background: c } }), p.loading ? _jsx(Spinner, { size: 18, c: c }) : p.icon !== undefined ? _jsx("span", { "aria-hidden": true, style: { fontSize: 18, lineHeight: '22px' }, children: str(p.icon) }) : null, _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [p.title !== undefined ? _jsx("div", { style: { fontWeight: 600 }, children: str(p.title) }) : null, _jsx("div", { style: { fontSize: 14, color: INK2, whiteSpace: 'pre-line' }, children: children })] }), p.withCloseButton === false ? null : (_jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: "\u00D7" }))] }));
535
+ if (!place)
536
+ return body;
537
+ const float = (_jsx("div", { style: place, children: _jsx(motion.div, { initial: animate ? { opacity: 0, y: place.top !== undefined ? -12 : 12 } : false, animate: { opacity: 1, y: 0 }, transition: { duration: 0.22 }, children: body }) }));
538
+ // 浮到弹窗层(跟着镜头),没有弹窗层(服务端渲染)时相对最近的定位祖先
539
+ return dialogLayer ? createPortal(float, dialogLayer) : float;
540
+ }
541
+ function Spinner({ size = 18, c = INK, kind = 'oval' }) {
542
+ const { animate } = useCtx();
543
+ if (kind === 'dots')
544
+ return (_jsx("span", { "aria-hidden": true, style: { display: 'inline-flex', gap: size / 6 }, children: [0, 1, 2].map((i) => (_jsx(motion.span, { style: { width: size / 4, height: size / 4, borderRadius: 99, background: c }, animate: animate ? { opacity: [0.3, 1, 0.3] } : undefined, transition: animate ? { repeat: Infinity, duration: 0.9, delay: i * 0.15 } : undefined }, i))) }));
545
+ return (_jsx(motion.span, { "aria-hidden": true, style: { display: 'inline-block', width: size, height: size, flexShrink: 0, borderRadius: 999, border: `2px solid ${c}`, borderTopColor: 'transparent' }, animate: animate ? { rotate: 360 } : undefined, transition: animate ? { repeat: Infinity, ease: 'linear', duration: 0.9 } : undefined }));
546
+ }
547
+ const LOADER = { xs: 14, sm: 18, md: 24, lg: 32, xl: 40 };
548
+ function Loader({ node, p, children }) {
549
+ const size = typeof p.size === 'number' ? p.size : (LOADER[str(p.size, 'md')] ?? 24);
550
+ const root = useRoot(node, p, { style: { display: 'inline-flex', alignItems: 'center', gap: 8, color: INK2 } });
551
+ return (_jsxs("div", { ...root, role: "status", children: [_jsx(Spinner, { size: size, c: color(p.color) ?? INK, kind: str(p.type, 'oval') }), children ? _jsx("span", { children: children }) : null] }));
552
+ }
553
+ const PROGRESS_H = { xs: 3, sm: 5, md: 8, lg: 12, xl: 16 };
554
+ function Progress({ node, p }) {
555
+ const v = Math.max(0, Math.min(100, Number(p.value ?? 0) || 0));
556
+ const h = typeof p.size === 'number' ? p.size : (PROGRESS_H[str(p.size, 'md')] ?? 8);
557
+ const c = color(p.color) ?? INK;
558
+ const root = useRoot(node, p, { className: 'jbox', style: { width: '100%', height: h + 4, background: SUBTLE, borderRadius: 999, overflow: 'hidden' } });
559
+ return (_jsxs("div", { ...root, role: "progressbar", "aria-valuenow": v, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT, strokeWidth: 1 }), _jsx("div", { style: { width: `${v}%`, height: '100%', background: c, backgroundImage: p.striped ? 'repeating-linear-gradient(45deg, rgba(255,255,255,.25) 0 6px, transparent 6px 12px)' : undefined } })] }));
560
+ }
561
+ function RingProgress({ node, p }) {
562
+ const size = typeof p.size === 'number' ? p.size : 120;
563
+ const th = typeof p.thickness === 'number' ? p.thickness : size / 10;
564
+ const rr = (size - th) / 2;
565
+ const L = 2 * Math.PI * rr;
566
+ let acc = 0;
567
+ const sections = arr(p.sections).filter((s) => !!s && typeof s === 'object');
568
+ const root = useRoot(node, p, { style: { position: 'relative', width: size, height: size, flexShrink: 0 } });
569
+ return (_jsxs("div", { ...root, role: "img", children: [_jsxs("svg", { width: size, height: size, style: { transform: 'rotate(-90deg)' }, children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: rr, fill: "none", style: { stroke: LINE }, strokeWidth: th }), sections.map((s, i) => {
570
+ const v = Math.max(0, Math.min(100, Number(s.value) || 0));
571
+ const el = _jsx("circle", { cx: size / 2, cy: size / 2, r: rr, fill: "none", stroke: color(s.color) ?? INK, strokeWidth: th, strokeDasharray: `${(L * v) / 100} ${L}`, strokeDashoffset: (-L * acc) / 100 }, i);
572
+ acc += v;
573
+ return el;
574
+ })] }), p.label !== undefined ? _jsx("span", { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, fontWeight: 600 }, children: str(p.label) }) : null] }));
575
+ }
576
+ function Skeleton({ node, p, children }) {
577
+ const { animate } = useCtx();
578
+ const r = useR(p);
579
+ const h = dim(r('height') ?? r('h')) ?? (p.circle ? '40px' : '16px');
580
+ const root = useRoot(node, p, { style: { width: p.circle ? h : (dim(r('width') ?? r('w')) ?? '100%'), height: h, borderRadius: p.circle ? 999 : (radius(p.radius) ?? '4px'), background: LINE } });
581
+ if (p.visible === false)
582
+ return _jsx(_Fragment, { children: children });
583
+ // D68:加载中轻轻呼吸(截图、导出与 animate=false 时静止)
584
+ const pulse = animate && p.animate !== false;
585
+ return _jsx(motion.div, { ...root, "aria-hidden": true, "data-jpulse": pulse ? '' : undefined, animate: pulse ? { opacity: [1, 0.45, 1] } : undefined, transition: pulse ? { repeat: Infinity, duration: 1.4, ease: 'easeInOut' } : undefined });
586
+ }
587
+ // ============ Modal / Drawer ============
588
+ const OVERLAY_SIZE = { xs: 320, sm: 380, md: 440, lg: 620, xl: 780 };
589
+ function sizeOf(v, d) {
590
+ if (typeof v === 'number')
591
+ return `${v}px`;
592
+ if (typeof v === 'string' && v)
593
+ return v in OVERLAY_SIZE ? `${OVERLAY_SIZE[v]}px` : v;
594
+ return `${d}px`;
595
+ }
596
+ /** Modal / Drawer:渲染到舞台内的弹窗层(不 portal 到 body),带遮罩。门控(opened)已由 isSelfVisible 判断。 */
597
+ function Overlay({ node, p, children }) {
598
+ const { t: i18n } = useI18n();
599
+ const { dialogLayer, animate } = useCtx();
600
+ const drawer = node.type === 'ui:Drawer';
601
+ const position = str(p.position, 'left');
602
+ const full = p.fullScreen === true;
603
+ const horizontal = drawer && (position === 'left' || position === 'right');
604
+ const title = p.title !== undefined ? str(p.title) : '';
605
+ const panelStyle = drawer
606
+ ? horizontal
607
+ ? { height: '100%', width: sizeOf(p.size, 440), maxWidth: '90%' }
608
+ : { width: '100%', height: sizeOf(p.size, 320), maxHeight: '90%' }
609
+ : full
610
+ ? { width: '100%', height: '100%' }
611
+ : { width: sizeOf(p.size, 440), maxWidth: '92%', maxHeight: '90%', ...(p.centered ? {} : { marginTop: '6%' }) };
612
+ const root = useRoot(node, p, {
613
+ className: cn('jbox relative z-10 flex flex-col bg-[var(--jp-surface)] shadow-[var(--jp-shadow)] [.jstage[data-stroke=clean]_&]:shadow-xl', !drawer && !full && '[.jstage[data-stroke=clean]_&]:rounded-lg'),
614
+ style: { ...panelStyle, overflow: 'auto' },
615
+ });
616
+ if (!dialogLayer)
617
+ return null;
618
+ const align = drawer
619
+ ? { alignItems: position === 'bottom' ? 'flex-end' : 'flex-start', justifyContent: position === 'right' ? 'flex-end' : 'flex-start', flexDirection: horizontal ? 'row' : 'column' }
620
+ : { alignItems: p.centered || full ? 'center' : 'flex-start', justifyContent: 'center' };
621
+ const initial = drawer ? { opacity: 0, x: position === 'left' ? -40 : position === 'right' ? 40 : 0, y: position === 'top' ? -40 : position === 'bottom' ? 40 : 0 } : { opacity: 0, y: 12, scale: 0.98 };
622
+ return createPortal(_jsxs("div", { className: "absolute inset-0 z-20 flex", style: align, "data-joverlay-root": node.id, children: [p.withOverlay === false ? null : (_jsx(motion.div, { "data-jbackdrop": "", className: "absolute inset-0 bg-[rgba(30,30,30,0.38)]", initial: animate ? { opacity: 0 } : false, animate: { opacity: 1 }, transition: { duration: 0.18 } })), _jsxs(motion.div, { ...root, role: "dialog", "aria-modal": "true", "aria-label": title || i18n.stage.modal, initial: animate ? initial : false, animate: { opacity: 1, x: 0, y: 0, scale: 1 }, transition: { duration: 0.22 }, children: [_jsx(Rough, { seedKey: node.id, strokeWidth: 1.8 }), title || p.withCloseButton !== false ? (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '16px 20px 8px' }, children: [_jsx("div", { style: { flex: 1, fontSize: 19, fontWeight: 700, lineHeight: 1.35 }, children: title }), p.withCloseButton === false ? null : (_jsx("span", { "aria-hidden": true, style: { color: DIMMED, fontSize: 20, lineHeight: 1 }, children: "\u00D7" }))] })) : null, _jsx("div", { style: { padding: '8px 20px 20px', display: 'flex', flexDirection: 'column', gap: 12 }, children: children })] })] }), dialogLayer);
623
+ }
624
+ // ============ 按钮 ============
625
+ const BTN_H = { xs: 30, sm: 36, md: 40, lg: 48, xl: 56, 'compact-xs': 22, 'compact-sm': 26, 'compact-md': 30, 'compact-lg': 34, 'compact-xl': 40 };
626
+ function buttonTone(variant, c) {
627
+ const base = c ?? INK;
628
+ switch (variant) {
629
+ case 'light':
630
+ return { stroke: light(base), fill: light(base), text: base };
631
+ case 'outline':
632
+ return { stroke: base, text: base };
633
+ case 'subtle':
634
+ case 'transparent':
635
+ return { stroke: 'transparent', text: base };
636
+ case 'white':
637
+ return { stroke: LINE, fill: SURFACE, text: base };
638
+ case 'default':
639
+ return { stroke: LINE_STRONG, fill: SURFACE, text: INK };
640
+ default:
641
+ return { stroke: base, fill: base, text: onFill(base) };
642
+ }
643
+ }
644
+ function Button({ node, p, disabled, children }) {
645
+ const mode = useStroke();
646
+ const variant = str(p.variant, 'filled');
647
+ const t = buttonTone(variant, color(p.color));
648
+ const h = BTN_H[str(p.size, 'md')] ?? 40;
649
+ const loading = p.loading === true;
650
+ const stroke = disabled ? SOFT : t.stroke;
651
+ const fill = disabled ? (t.fill && t.fill !== SURFACE ? LINE_STRONG : undefined) : t.fill;
652
+ const text = disabled ? (t.fill && t.fill !== SURFACE ? INK2 : DIMMED) : t.text;
653
+ const root = useRoot(node, p, {
654
+ disabled,
655
+ className: cn('jbox', mode === 'clean' && 'rounded-md', disabled && 'cursor-not-allowed'),
656
+ style: {
657
+ display: 'inline-flex',
658
+ alignItems: 'center',
659
+ justifyContent: 'center',
660
+ gap: 6,
661
+ height: h,
662
+ padding: `0 ${Math.round(h * 0.45)}px`,
663
+ fontSize: h <= 30 ? 13 : 15,
664
+ fontWeight: 600,
665
+ color: text,
666
+ maxWidth: '100%',
667
+ width: p.fullWidth ? '100%' : undefined,
668
+ flexShrink: 0,
669
+ ...(mode === 'clean' ? { ...boxStyle(stroke, fill), ...(disabled ? { borderStyle: 'dashed' } : {}) } : {}),
670
+ },
671
+ });
672
+ return (_jsxs("button", { ...root, type: "button", "data-dashed": disabled ? '' : undefined, children: [variant === 'subtle' || variant === 'transparent' ? null : _jsx(Rough, { seedKey: node.id, stroke: stroke, fill: fill, fillStyle: fill ? 'solid' : undefined, dashed: disabled, strokeWidth: 1.4 }), loading ? _jsx(Spinner, { size: 16, c: text }) : null, p.leftSection !== undefined ? _jsx("span", { "aria-hidden": true, children: str(p.leftSection) }) : null, _jsx("span", { className: "jtrunc", "data-jtext": "", children: children }), p.rightSection !== undefined ? _jsx("span", { "aria-hidden": true, children: str(p.rightSection) }) : null] }));
673
+ }
674
+ const ICON_SIZE = { xs: 18, sm: 22, md: 28, lg: 34, xl: 44 };
675
+ function ActionIcon({ node, p, disabled, children }) {
676
+ const mode = useStroke();
677
+ const t = buttonTone(str(p.variant, 'filled'), color(p.color));
678
+ const size = typeof p.size === 'number' ? p.size : (ICON_SIZE[str(p.size, 'md')] ?? 28);
679
+ const round = p.radius === 'xl' || p.radius === 999;
680
+ const fill = disabled ? LINE_STRONG : t.fill;
681
+ const root = useRoot(node, p, {
682
+ disabled,
683
+ className: cn('jbox', disabled && 'cursor-not-allowed'),
684
+ style: { width: size, height: size, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, color: disabled ? INK2 : t.text, fontSize: size * 0.5, fontWeight: 700, borderRadius: round ? 999 : (radius(p.radius) ?? 6), ...(mode === 'clean' ? boxStyle(disabled ? SOFT : t.stroke, fill) : {}) },
685
+ });
686
+ return (_jsxs("button", { ...root, type: "button", "aria-label": str(p['aria-label'], nodeLabel(node)), children: [_jsx(Rough, { seedKey: node.id, shape: round ? 'ellipse' : 'rect', stroke: disabled ? SOFT : t.stroke, fill: fill, fillStyle: fill ? 'solid' : undefined, strokeWidth: 1.3, inset: 1 }), _jsx("span", { "data-jtext": "", children: children })] }));
687
+ }
688
+ function CloseButton({ node, p, disabled }) {
689
+ const { t: i18n } = useI18n();
690
+ const root = useRoot(node, p, { disabled, style: { width: 28, height: 28, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: DIMMED, fontSize: 20, lineHeight: 1, flexShrink: 0 } });
691
+ return (_jsx("button", { ...root, type: "button", "aria-label": str(p['aria-label'], i18n.stage.close), children: "\u00D7" }));
692
+ }
693
+ function NavLink({ node, p, disabled, children }) {
694
+ const { scene } = useCtx();
695
+ const active = p.active === true || (p.active === undefined && typeof p.href === 'string' && p.href === scene.route);
696
+ const c = color(p.color) ?? INK;
697
+ const opened = p.opened === true || p.defaultOpened === true;
698
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', flexDirection: 'column' } });
699
+ return (_jsxs("div", { ...root, "aria-current": active ? 'page' : undefined, children: [_jsxs("a", { role: "link", tabIndex: 0, className: cn('jbox flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-[15px] [.jstage[data-stroke=clean]_&]:rounded-md', !active && 'hover:bg-black/[0.03]'), style: { background: active ? light(c === INK ? INK2 : c) : undefined, fontWeight: active ? 650 : 400, color: disabled ? DIMMED : active && c !== INK ? c : undefined }, "data-noborder": active ? undefined : '', "data-soft": "", "data-jcontrol": node.id, children: [active ? _jsx(Rough, { seedKey: node.id, stroke: SOFT, fill: LINE, fillStyle: "solid", strokeWidth: 1 }) : null, p.leftSection !== undefined ? _jsx("span", { "aria-hidden": true, children: str(p.leftSection) }) : null, _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "jtrunc", "data-jtext": "", children: str(p.label) || null }), p.description !== undefined ? _jsx("span", { style: { fontSize: 12, color: DIMMED }, children: str(p.description) }) : null] }), p.rightSection !== undefined ? _jsx("span", { "aria-hidden": true, children: str(p.rightSection) }) : (node.children ?? []).length ? _jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: opened ? '▾' : '▸' }) : null] }), opened && (node.children ?? []).length ? _jsx("div", { style: { paddingLeft: 16 }, children: children }) : null] }));
700
+ }
701
+ // ============ 输入 ============
702
+ /** Mantine 输入组件的外壳:label(withAsterisk)/ description / 控件 / error。根节点包含全部,控件带 data-jcontrol。 */
703
+ function Field({ node, p, disabled, control, inline = false }) {
704
+ const root = useRoot(node, p, { disabled, style: inline ? { display: 'flex', alignItems: 'flex-start', gap: 10 } : { display: 'flex', flexDirection: 'column', gap: 4, width: '100%' } });
705
+ const ctx = useCtx();
706
+ const toggle = node.traits?.native?.action === 'toggle';
707
+ if (ctx.interactive && toggle) {
708
+ const activate = () => { if (!disabled)
709
+ ctx.act?.(node.id, { type: 'click' }, { type: 'toggle', target: node.id, value: !Boolean(ctx.state.inputs[node.id] ?? p.checked ?? p.defaultChecked) }); };
710
+ Object.assign(root, { onClick: (e) => { e.stopPropagation(); activate(); }, onKeyDown: (e) => { if (e.key === ' ' || e.key === 'Enter') {
711
+ e.preventDefault();
712
+ e.stopPropagation();
713
+ activate();
714
+ } } });
715
+ }
716
+ const label = p.label !== undefined && p.label !== '' ? str(p.label) : '';
717
+ const desc = p.description !== undefined ? str(p.description) : '';
718
+ const error = typeof p.error === 'string' && p.error ? p.error : '';
719
+ const star = p.withAsterisk === true || p.required === true;
720
+ const head = label ? (_jsxs("span", { style: { fontSize: 14, fontWeight: 500, lineHeight: 1.4 }, children: [label, star ? _jsx("span", { style: { color: DANGER }, children: " *" }) : null] })) : null;
721
+ if (inline)
722
+ return (_jsxs("div", { ...root, children: [control, label || desc ? (_jsxs("span", { style: { display: 'flex', flexDirection: 'column' }, children: [head, desc ? _jsx("span", { style: { fontSize: 12, color: DIMMED }, children: desc }) : null] })) : null] }));
723
+ return (_jsxs("div", { ...root, children: [head, desc ? _jsx("span", { style: { fontSize: 12, color: DIMMED, marginTop: -2 }, children: desc }) : null, control, error ? _jsx("span", { style: { fontSize: 12, color: DANGER }, children: error }) : null] }));
724
+ }
725
+ /** 输入框外观:手绘描边 / 线框边框;error 时描红。 */
726
+ function InputBox({ node, p, disabled, children, minH = 40, open }) {
727
+ const err = typeof p.error === 'string' && p.error !== '';
728
+ return (_jsxs("div", { className: "jbox flex w-full items-center gap-2 px-3 [.jstage[data-stroke=clean]_&]:rounded-md", style: { minHeight: minH, background: disabled ? SUBTLE : SURFACE, ...(err ? { borderColor: DANGER } : {}) }, "data-jcontrol": node.id, role: open !== undefined ? 'combobox' : undefined, "aria-label": open !== undefined ? nodeLabel(node) : undefined, "aria-expanded": open, tabIndex: 0, children: [_jsx(Rough, { seedKey: node.id, stroke: err ? DANGER : disabled ? SOFT : INK }), p.leftSection !== undefined ? _jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: str(p.leftSection) }) : null, children, p.rightSection !== undefined ? _jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: str(p.rightSection) }) : null] }));
729
+ }
730
+ const FIELD_CLASS = 'min-w-0 flex-1 cursor-[inherit] resize-none border-none bg-transparent p-0 text-[15px] text-inherit outline-none placeholder:text-[var(--jp-faint)]';
731
+ /** 只读的原生输入框:值来自状态,用户不能直接编辑(输入由步骤驱动)。 */
732
+ function NativeInput({ node, disabled, label, value, placeholder, mask = false }) {
733
+ const ctx = useCtx();
734
+ return _jsx("input", { readOnly: !ctx.interactive || disabled, tabIndex: ctx.interactive && !disabled ? 0 : -1, onChange: e => ctx.act?.(node.id, { type: 'type', value: e.currentTarget.value }), onClick: ctx.interactive ? e => e.stopPropagation() : undefined, "aria-label": label, type: mask ? 'password' : 'text', value: str(value), placeholder: str(placeholder), className: cn(FIELD_CLASS, 'h-9'), style: { font: 'inherit', fontSize: 15 } });
735
+ }
736
+ function TextInput({ node, p, disabled }) {
737
+ const v = useInputValue(node, p);
738
+ const mask = node.type === 'ui:PasswordInput' || p.type === 'password';
739
+ const prefix = p.prefix !== undefined ? str(p.prefix) : '';
740
+ const suffix = p.suffix !== undefined ? str(p.suffix) : '';
741
+ const has = v !== undefined && v !== null && v !== '';
742
+ const value = has && (prefix || suffix) ? `${prefix}${str(v)}${suffix}` : v;
743
+ return (_jsx(Field, { node: node, p: p, disabled: disabled, control: _jsxs(InputBox, { node: node, p: p, disabled: disabled, children: [_jsx(NativeInput, { node: node, disabled: disabled, label: nodeLabel(node), value: value, placeholder: p.placeholder, mask: mask }), node.type === 'ui:PasswordInput' ? _jsx("span", { "aria-hidden": true, style: { color: DIMMED, fontSize: 13 }, children: "\uD83D\uDC41" }) : null, node.type === 'ui:NumberInput' ? _jsxs("span", { "aria-hidden": true, style: { color: DIMMED, fontSize: 10, lineHeight: 1 }, children: ["\u25B4", _jsx("br", {}), "\u25BE"] }) : null, node.type === 'ui:FileInput' && !has ? _jsx("span", { "aria-hidden": true, style: { color: DIMMED, fontSize: 13 }, children: "\uD83D\uDCCE" }) : null] }) }));
744
+ }
745
+ function Textarea({ node, p, disabled }) {
746
+ const ctx = useCtx();
747
+ const v = useInputValue(node, p);
748
+ const rows = typeof p.minRows === 'number' ? p.minRows : typeof p.rows === 'number' ? p.rows : 3;
749
+ return (_jsx(Field, { node: node, p: p, disabled: disabled, control: _jsx(InputBox, { node: node, p: p, disabled: disabled, minH: rows * 22 + 16, children: _jsx("textarea", { readOnly: !ctx.interactive || disabled, tabIndex: ctx.interactive && !disabled ? 0 : -1, onChange: e => ctx.act?.(node.id, { type: 'type', value: e.currentTarget.value }), onClick: ctx.interactive ? e => e.stopPropagation() : undefined, "aria-label": nodeLabel(node), value: str(v), placeholder: str(p.placeholder), rows: rows, className: cn(FIELD_CLASS, 'self-stretch py-2'), style: { font: 'inherit', fontSize: 15, lineHeight: '22px' } }) }) }));
750
+ }
751
+ /** Select / MultiSelect:选项来自 data;显示 inputs[id];选择动画期间展开下拉。 */
752
+ function Select({ node, p, disabled }) {
753
+ const { t: i18n } = useI18n();
754
+ const ctx = useCtx();
755
+ const { state, preview } = ctx;
756
+ const [localOpen, setLocalOpen] = useState(false);
757
+ useLayoutEffect(() => setLocalOpen(false), [ctx.resetKey]);
758
+ const v = useInputValue(node, p);
759
+ const choices = choicesOf(node, state) ?? [];
760
+ const multi = node.type === 'ui:MultiSelect';
761
+ const values = multi ? (Array.isArray(v) ? v.map(String) : []) : v !== undefined && v !== null && v !== '' ? [str(v)] : [];
762
+ const labelOf = (x) => choices.find((c) => c.value === x)?.label ?? x;
763
+ const pv = preview && preview.nodeId === node.id ? preview.text : null;
764
+ const open = pv !== null || !!ctx.interactive && localOpen;
765
+ const previewChoice = pv !== null ? (choices.find((c) => c.value === pv || c.label === pv)?.value ?? null) : null;
766
+ return (_jsx(Field, { node: node, p: p, disabled: disabled, control: _jsxs("div", { className: "relative w-full", onClick: ctx.interactive ? e => { e.stopPropagation(); if (!disabled) {
767
+ setLocalOpen(v => !v);
768
+ ctx.act?.(node.id, { type: 'click' });
769
+ } } : undefined, onKeyDown: ctx.interactive ? e => {
770
+ if (disabled)
771
+ return;
772
+ if (e.key === 'Escape') {
773
+ e.stopPropagation();
774
+ setLocalOpen(false);
775
+ }
776
+ if (e.key === 'Enter' || e.key === ' ') {
777
+ e.preventDefault();
778
+ e.stopPropagation();
779
+ setLocalOpen(v => !v);
780
+ }
781
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
782
+ e.preventDefault();
783
+ e.stopPropagation();
784
+ setLocalOpen(true);
785
+ const index = choices.findIndex(c => c.value === values[0]);
786
+ const next = choices[(index + (e.key === 'ArrowDown' ? 1 : choices.length - 1) + choices.length) % choices.length];
787
+ if (next)
788
+ ctx.act?.(node.id, { type: 'select', value: next.value });
789
+ }
790
+ } : undefined, children: [_jsxs(InputBox, { node: node, p: p, disabled: disabled, open: open, children: [multi && values.length ? (_jsx("span", { className: "flex min-w-0 flex-1 flex-wrap gap-1 py-1.5", children: values.map((x) => (_jsxs("span", { style: { background: SUBTLE, borderRadius: 4, padding: '0 6px', fontSize: 13 }, children: [labelOf(x), " \u00D7"] }, x))) })) : (_jsx("span", { className: cn('jtrunc min-w-0 flex-1', values.length ? '' : 'text-[var(--jp-faint)]'), "data-jtext": "", children: values.length ? labelOf(values[0]) : str(p.placeholder, i18n.stage.selectAnOption) })), _jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: "\u25BE" })] }), open && choices.length ? (_jsxs("div", { className: "jbox absolute inset-x-0 top-[calc(100%+4px)] z-30 flex flex-col bg-[var(--jp-surface)] py-1 shadow-[var(--jp-shadow)] [.jstage[data-stroke=clean]_&]:rounded-md", role: "listbox", "data-jdropdown": "", children: [_jsx(Rough, { seedKey: `${node.id}:list`, stroke: INK2, strokeWidth: 1.1 }), choices.map((c) => {
791
+ const on = c.value === previewChoice || values.includes(c.value);
792
+ return (_jsxs("div", { role: "option", tabIndex: ctx.interactive ? 0 : undefined, onClick: ctx.interactive ? e => { e.stopPropagation(); ctx.act?.(node.id, { type: 'select', value: c.value }); if (!multi)
793
+ setLocalOpen(false); } : undefined, onKeyDown: ctx.interactive ? e => { if (e.key === 'Enter' || e.key === ' ') {
794
+ e.preventDefault();
795
+ e.stopPropagation();
796
+ ctx.act?.(node.id, { type: 'select', value: c.value });
797
+ if (!multi)
798
+ setLocalOpen(false);
799
+ } } : undefined, "aria-selected": on, className: cn('px-3 py-1.5 text-[15px]', on && 'bg-[var(--jp-subtle)] font-semibold'), children: [multi && values.includes(c.value) ? '✓ ' : '', c.label] }, c.value));
800
+ })] })) : null] }) }));
801
+ }
802
+ function SegmentedControl({ node, p, disabled }) {
803
+ const ctx = useCtx();
804
+ const { state, preview } = ctx;
805
+ const v = useInputValue(node, p);
806
+ const choices = choicesOf(node, state) ?? [];
807
+ const pv = preview && preview.nodeId === node.id ? preview.text : null;
808
+ const cur = pv !== null ? (choices.find((c) => c.value === pv || c.label === pv)?.value ?? null) : v !== undefined && v !== null && v !== '' ? str(v) : (choices[0]?.value ?? null);
809
+ const root = useRoot(node, p, {
810
+ disabled,
811
+ className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md',
812
+ style: { display: p.fullWidth ? 'flex' : 'inline-flex', width: p.fullWidth ? '100%' : 'fit-content', padding: 4, gap: 2, background: SUBTLE },
813
+ });
814
+ return (_jsxs("div", { ...root, role: "radiogroup", "data-soft": "", "data-jcontrol": node.id, children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT, strokeWidth: 1 }), choices.map((c) => {
815
+ const on = c.value === cur;
816
+ return (_jsxs("span", { role: "radio", tabIndex: ctx.interactive && !disabled ? 0 : undefined, onClick: ctx.interactive ? e => { e.stopPropagation(); if (!disabled)
817
+ ctx.act?.(node.id, { type: 'click' }, { type: 'select', target: node.id, value: c.value }); } : undefined, onKeyDown: ctx.interactive ? e => { if (e.key === ' ' || e.key === 'Enter') {
818
+ e.preventDefault();
819
+ e.stopPropagation();
820
+ if (!disabled)
821
+ ctx.act?.(node.id, { type: 'select', value: c.value });
822
+ } } : undefined, "aria-checked": on, className: cn('relative px-3 py-1 text-[14px] [.jstage[data-stroke=clean]_&]:rounded', on ? 'bg-[var(--jp-surface)] font-semibold shadow-sm' : 'text-[var(--jp-muted)]'), style: { flex: p.fullWidth ? 1 : undefined, textAlign: 'center' }, children: [on ? _jsx(Rough, { seedKey: `${node.id}:${c.value}`, stroke: INK, strokeWidth: 1.1 }) : null, _jsx("span", { className: "relative", children: c.label })] }, c.value));
823
+ })] }));
824
+ }
825
+ function Checkbox({ node, p, disabled }) {
826
+ const checked = useInputValue(node, p) === true;
827
+ return (_jsx(Field, { node: node, p: p, disabled: disabled, inline: true, control: _jsxs("span", { className: "jbox inline-flex shrink-0 items-center justify-center rounded-[3px]", style: { width: 20, height: 20, marginTop: 1, background: checked ? INK : SURFACE, color: BG, fontSize: 13, lineHeight: 1 }, "data-jcontrol": node.id, role: "checkbox", "aria-label": nodeLabel(node), "aria-checked": checked, tabIndex: 0, children: [_jsx(Rough, { seedKey: node.id, inset: 1, stroke: disabled ? SOFT : INK, fill: checked ? INK : undefined, fillStyle: "solid" }), checked ? _jsx("span", { children: "\u2713" }) : null] }) }));
828
+ }
829
+ function Switch({ node, p, disabled }) {
830
+ const on = useInputValue(node, p) === true;
831
+ const ctx = useCtx();
832
+ const scripted = useRef({ value: on, changedLocally: false });
833
+ const changedLocally = !!ctx.playground && on !== scripted.current.value;
834
+ const instant = changedLocally || scripted.current.changedLocally;
835
+ if (!ctx.playground)
836
+ scripted.current = { value: on, changedLocally: false };
837
+ else if (changedLocally)
838
+ scripted.current.changedLocally = true;
839
+ const c = color(p.color) ?? INK;
840
+ return (_jsx(Field, { node: node, p: p, disabled: disabled, inline: true, control: _jsx("span", { className: "relative inline-flex shrink-0 items-center rounded-full", style: { width: 38, height: 22, background: on ? c : LINE, transition: instant ? 'none' : 'background .2s', marginTop: 1 }, "data-jcontrol": node.id, role: "switch", "aria-label": nodeLabel(node), "aria-checked": on, tabIndex: 0, children: _jsx("span", { style: { position: 'absolute', left: on ? 18 : 2, width: 18, height: 18, borderRadius: 9, background: SURFACE, boxShadow: '0 1px 2px rgba(0,0,0,.25)', transition: instant ? 'none' : 'left .2s' } }) }) }));
841
+ }
842
+ function RadioGroup({ node, p, disabled, children }) {
843
+ const v = useInputValue(node, p);
844
+ const label = p.label !== undefined ? str(p.label) : '';
845
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', flexDirection: 'column', gap: 8 } });
846
+ return (_jsx(ValueContext.Provider, { value: { owner: node.id, value: v === undefined || v === null ? null : str(v), disabled }, children: _jsxs("div", { ...root, role: "radiogroup", children: [label ? _jsx("span", { style: { fontSize: 14, fontWeight: 500 }, children: label }) : null, p.description !== undefined ? _jsx("span", { style: { fontSize: 12, color: DIMMED, marginTop: -4 }, children: str(p.description) }) : null, _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, "data-jcontrol": node.id, children: children })] }) }));
847
+ }
848
+ function Radio({ node, p, disabled }) {
849
+ const host = useCtx();
850
+ const ctx = useContext(ValueContext);
851
+ const standalone = useInputValue(node, p) === true;
852
+ const on = ctx ? ctx.value !== null && ctx.value === str(p.value) : standalone;
853
+ const root = useRoot(node, p, { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } });
854
+ return (_jsxs("span", { ...root, role: "radio", tabIndex: host.interactive ? 0 : undefined, onClick: host.interactive ? e => { e.stopPropagation(); if (!disabled && !ctx?.disabled)
855
+ host.act?.(node.id, { type: 'click' }, ctx ? { type: 'select', target: ctx.owner, value: str(p.value) } : { type: 'toggle', target: node.id, value: !on }); } : undefined, onKeyDown: host.interactive ? e => { if (e.key === ' ' || e.key === 'Enter') {
856
+ e.preventDefault();
857
+ e.stopPropagation();
858
+ e.currentTarget.click();
859
+ } } : undefined, "aria-checked": on, children: [_jsxs("span", { className: "jbox inline-flex shrink-0 items-center justify-center rounded-full", style: { width: 20, height: 20 }, children: [_jsx(Rough, { seedKey: node.id, shape: "ellipse", inset: 1 }), on ? _jsx("span", { style: { width: 9, height: 9, borderRadius: 5, background: INK } }) : null] }), p.label !== undefined ? _jsx("span", { style: { fontSize: 14 }, children: str(p.label) }) : null] }));
860
+ }
861
+ // ============ Tabs / Breadcrumbs / Pagination / Stepper ============
862
+ function Tabs({ node, p, disabled, children }) {
863
+ const { state } = useCtx();
864
+ const v = useInputValue(node, p);
865
+ const first = choicesOf(node, state)?.[0]?.value ?? null;
866
+ const value = v !== undefined && v !== null && v !== '' ? str(v) : first;
867
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', flexDirection: 'column', gap: 12 } });
868
+ return (_jsx(ValueContext.Provider, { value: { owner: node.id, value, disabled }, children: _jsx("div", { ...root, children: children }) }));
869
+ }
870
+ function TabsList({ node, p, children }) {
871
+ const root = useRoot(node, p, { className: p.grow ? 'jgrow' : undefined, style: { display: 'flex', gap: 2, borderBottom: `2px solid ${LINE}`, justifyContent: p.grow ? 'stretch' : flexVal(p.justify, 'flex-start') } });
872
+ return (_jsx("div", { ...root, role: "tablist", children: children }));
873
+ }
874
+ function TabsTab({ node, p, disabled, children }) {
875
+ const host = useCtx();
876
+ const ctx = useContext(ValueContext);
877
+ const dis = disabled || !!ctx?.disabled;
878
+ const active = ctx?.value !== null && ctx?.value === str(p.value);
879
+ const root = useRoot(node, p, { disabled: dis, style: { position: 'relative', padding: '8px 14px', marginBottom: -2, fontSize: 14, borderBottom: `2px solid ${active ? INK : 'transparent'}`, fontWeight: active ? 650 : 400, color: dis ? SOFT : active ? INK : DIMMED, display: 'inline-flex', gap: 6, alignItems: 'center' } });
880
+ return (_jsxs("button", { ...root, type: "button", role: "tab", "aria-selected": active, onClick: host.interactive ? e => { e.stopPropagation(); if (!dis && ctx)
881
+ host.act?.(node.id, { type: 'click' }, { type: 'select', target: ctx.owner, value: str(p.value) }); } : undefined, children: [p.leftSection !== undefined ? _jsx("span", { "aria-hidden": true, children: str(p.leftSection) }) : null, _jsx("span", { className: "jtrunc", "data-jtext": "", children: children }), p.rightSection !== undefined && p.rightSection !== null && p.rightSection !== '' ? (_jsx("span", { style: { minWidth: 18, height: 18, padding: '0 6px', borderRadius: 999, background: active ? INK : SUBTLE, color: active ? BG : DIMMED, fontSize: 11, lineHeight: '18px', textAlign: 'center', fontWeight: 600 }, children: str(p.rightSection) })) : null] }));
882
+ }
883
+ function TabsPanel({ node, p, children }) {
884
+ const ctx = useContext(ValueContext);
885
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 12 } });
886
+ if (ctx && ctx.value !== str(p.value))
887
+ return null;
888
+ return (_jsx("div", { ...root, role: "tabpanel", children: children }));
889
+ }
890
+ function Breadcrumbs({ node, p, children }) {
891
+ const { t: i18n } = useI18n();
892
+ const sep = p.separator !== undefined ? str(p.separator) : '/';
893
+ const items = (Array.isArray(children) ? children : [children]).filter(Boolean);
894
+ const root = useRoot(node, p, { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, fontSize: 14 } });
895
+ return (_jsx("nav", { ...root, "aria-label": i18n.stage.breadcrumbs, children: items.map((c, i) => (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 8 }, children: [i > 0 ? (_jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: sep })) : null, c] }, i))) }));
896
+ }
897
+ function Pagination({ node, p, disabled }) {
898
+ const total = Math.max(1, Number(p.total ?? 1) || 1);
899
+ const v = Number(useInputValue(node, p) ?? 1) || 1;
900
+ const pages = [];
901
+ for (let i = 1; i <= total; i++)
902
+ if (i === 1 || i === total || Math.abs(i - v) <= 1)
903
+ pages.push(i);
904
+ else if (pages[pages.length - 1] !== '…')
905
+ pages.push('…');
906
+ const cell = (label, on = false, key) => (_jsxs("span", { className: "jbox inline-flex items-center justify-center [.jstage[data-stroke=clean]_&]:rounded", style: { minWidth: 32, height: 32, padding: '0 6px', fontSize: 14, background: on ? INK : SURFACE, color: on ? BG : INK }, "data-soft": on ? undefined : '', children: [_jsx(Rough, { seedKey: `${node.id}:${key ?? label}`, stroke: on ? INK : SOFT, fill: on ? INK : undefined, fillStyle: "solid", strokeWidth: 1 }), _jsx("span", { children: label })] }, key ?? label));
907
+ const root = useRoot(node, p, { disabled, style: { display: 'inline-flex', gap: 6, alignItems: 'center' } });
908
+ return (_jsxs("div", { ...root, role: "navigation", "data-jcontrol": node.id, children: [cell('‹', false, 'prev'), pages.map((x, i) => (x === '…' ? _jsx("span", { children: "\u2026" }, `e${i}`) : cell(String(x), x === v, x))), cell('›', false, 'next')] }));
909
+ }
910
+ function Stepper({ node, p, children }) {
911
+ const { state } = useCtx();
912
+ const active = Number(resolveValue(p.active, state) ?? 0);
913
+ const ids = (node.children ?? []).filter((c) => c.type === 'ui:Stepper.Step').map((c) => c.id);
914
+ const [content, setContent] = useState(null);
915
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 16 } });
916
+ return (_jsx(ItemProgressContext.Provider, { value: { ids, active: Number.isFinite(active) ? active : 0, content }, children: _jsxs("div", { ...root, children: [_jsx("div", { style: { display: 'flex', alignItems: 'flex-start', gap: 8 }, children: children }), _jsx("div", { ref: setContent })] }) }));
917
+ }
918
+ function StepperStep({ node, p, children }) {
919
+ const seq = useContext(ItemProgressContext);
920
+ const i = seq ? seq.ids.indexOf(node.id) : 0;
921
+ const done = !!seq && i < seq.active;
922
+ const cur = !!seq && i === seq.active;
923
+ const last = !!seq && i === seq.ids.length - 1;
924
+ const root = useRoot(node, p, { style: { display: 'flex', alignItems: 'flex-start', gap: 8, flex: last ? '0 0 auto' : '1 1 0', minWidth: 0 } });
925
+ return (_jsxs("div", { ...root, "data-active": cur ? '' : undefined, children: [_jsxs("span", { "aria-hidden": true, className: "jbox inline-flex shrink-0 items-center justify-center rounded-full", style: { width: 30, height: 30, background: done ? INK : SURFACE, color: done ? BG : INK, fontSize: 13, fontWeight: 700, borderColor: cur || done ? INK : LINE_STRONG }, children: [_jsx(Rough, { seedKey: node.id, shape: "ellipse", stroke: cur || done ? INK : SOFT, fill: done ? INK : undefined, fillStyle: "solid", inset: 1 }), _jsx("span", { children: done ? '✓' : i + 1 })] }), _jsxs("span", { style: { display: 'flex', flexDirection: 'column', minWidth: 0 }, children: [p.label !== undefined ? _jsx("span", { style: { fontWeight: cur ? 700 : 500, fontSize: 14 }, children: str(p.label) }) : null, p.description !== undefined ? _jsx("span", { style: { fontSize: 12, color: DIMMED }, children: str(p.description) }) : null] }), last ? null : _jsx("span", { "aria-hidden": true, style: { flex: 1, height: 2, background: done ? INK : LINE, marginTop: 14, minWidth: 12 } }), cur && seq?.content ? createPortal(_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 12 }, children: children }), seq.content) : null] }));
926
+ }
927
+ // ============ AI 交互 ============
928
+ function ChatMessage({ node, p, children }) {
929
+ const user = p.from === 'user';
930
+ const root = useRoot(node, p, { style: { display: 'flex', width: '100%', justifyContent: user ? 'flex-end' : 'flex-start' } });
931
+ return (_jsx("div", { ...root, "data-from": user ? 'user' : 'assistant', children: _jsxs("div", { className: cn('flex max-w-[82%] flex-col gap-2', user && 'jbox px-4 py-2.5 [.jstage[data-stroke=clean]_&]:rounded-lg'), style: { whiteSpace: 'pre-wrap', lineHeight: 1.6, ...(user ? { background: SUBTLE } : { width: '100%' }) }, "data-soft": "", children: [user ? _jsx(Rough, { seedKey: node.id, stroke: SOFT, fill: SUBTLE, fillStyle: "solid" }) : null, p.name !== undefined ? _jsx("span", { style: { fontSize: 12, color: DIMMED }, children: str(p.name) }) : null, children] }) }));
932
+ }
933
+ const TOOL_STATE = (i18n) => ({
934
+ running: { text: i18n.stage.running, c: INFO },
935
+ done: { text: i18n.stage.done, c: OK },
936
+ error: { text: i18n.stage.error, c: DANGER },
937
+ 'input-streaming': { text: i18n.stage.preparing, c: DIMMED },
938
+ 'input-available': { text: i18n.stage.running, c: INFO },
939
+ 'output-available': { text: i18n.stage.done, c: OK },
940
+ 'output-error': { text: i18n.stage.error, c: DANGER },
941
+ });
942
+ function ToolCall({ node, p, children }) {
943
+ const { t: i18n } = useI18n();
944
+ const s = TOOL_STATE(i18n)[str(p.state, 'done')] ?? TOOL_STATE(i18n).done;
945
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { display: 'flex', flexDirection: 'column', gap: 4, width: '100%', background: SUBTLE, padding: '8px 12px', fontFamily: MONO, fontSize: 13 } });
946
+ return (_jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT }), _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', gap: 8 }, children: [_jsxs("span", { className: "jtrunc", children: ["\u2699 ", str(p.title, i18n.stage.toolCall)] }), _jsx("span", { style: { color: s.c, flexShrink: 0 }, children: s.text })] }), _jsx("div", { style: { whiteSpace: 'pre-wrap', color: INK2 }, children: children })] }));
947
+ }
948
+ /**
949
+ * 思考过程(D56):标题是此刻在想什么;active 时标题闪动、前面转圈;结束后显示“思考了 {duration}”。
950
+ * opened 时展开子节点(过程),左侧一条细线。
951
+ */
952
+ function Thinking({ node, p, children }) {
953
+ const { t: i18n } = useI18n();
954
+ const active = p.active === true;
955
+ const opened = p.opened === true;
956
+ const title = str(p.title);
957
+ const duration = str(p.duration);
958
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%', fontSize: 14, color: DIMMED } });
959
+ return (_jsxs("div", { ...root, "data-active": active ? '' : undefined, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }, children: [active ? _jsx(Spinner, { size: 14, c: DIMMED }) : _jsx("span", { "aria-hidden": true, style: { width: 14, textAlign: 'center', flexShrink: 0 }, children: "\u2726" }), _jsx("span", { className: cn('jtrunc', active && 'jshimmer'), style: { color: active ? undefined : INK2, fontWeight: 500 }, children: title || (active ? i18n.stage.thinkingActive : i18n.stage.thinking) }), !active && duration ? _jsx("span", { style: { flexShrink: 0, color: DIMMED }, children: i18n.stage.thoughtFor(duration) }) : null, children ? _jsx("span", { "aria-hidden": true, style: { flexShrink: 0, fontSize: 11 }, children: opened ? '▲' : '▼' }) : null] }), opened && children ? (_jsx("div", { style: { marginLeft: 6, paddingLeft: 14, borderLeft: `2px solid ${LINE}`, display: 'flex', flexDirection: 'column', gap: 6, whiteSpace: 'pre-wrap', lineHeight: 1.6 }, children: children })) : null] }));
960
+ }
961
+ /** 简单外部邮件:自身不可操作,正文里的按钮仍走普通步骤。 */
962
+ function Email({ node, p, children }) {
963
+ const { t: i18n } = useI18n();
964
+ const root = useRoot(node, p, { className: 'jbox', style: { background: SURFACE, padding: 16, display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 } });
965
+ const from = str(p.from, i18n.stage.sender);
966
+ return _jsxs("article", { ...root, children: [_jsx(Rough, { seedKey: node.id }), _jsxs("div", { style: { display: 'flex', gap: 10, alignItems: 'flex-start' }, children: [_jsx("span", { "aria-hidden": true, style: { width: 34, height: 34, flexShrink: 0, display: 'grid', placeItems: 'center', borderRadius: '50%', background: SUBTLE, color: INK, fontWeight: 600 }, children: [...from.trim()][0] ?? '?' }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 600 }, children: from }), _jsx("div", { style: { color: DIMMED, fontSize: 12 }, children: i18n.stage.recipient(str(p.to, i18n.stage.unspecified)) })] }), p.time ? _jsx("time", { style: { color: DIMMED, fontSize: 12 }, children: str(p.time) }) : null] }), _jsx("strong", { style: { fontSize: 18 }, children: str(p.subject, i18n.stage.noSubject) }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, lineHeight: 1.6, whiteSpace: 'pre-wrap' }, children: children })] });
967
+ }
968
+ function DataCard({ node, p }) {
969
+ const { t: i18n } = useI18n();
970
+ const { changed = [] } = useCtx();
971
+ const rows = dataRows(p.value, p.fields, node.props?.value, changed);
972
+ const root = useRoot(node, p, { className: 'jbox', style: { background: SURFACE, padding: 20, minWidth: 0, width: '100%' } });
973
+ return _jsxs("section", { ...root, children: [_jsx(Rough, { seedKey: node.id }), _jsx("div", { style: { fontWeight: 600, fontSize: 20, marginBottom: 14 }, children: str(p.title, i18n.stage.dataRecords) }), rows.length ? _jsx("dl", { style: { margin: 0, display: 'flex', flexDirection: 'column', gap: 4 }, children: rows.map((row, i) => _jsxs("div", { "data-changed": row.change ? '' : undefined, style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 12, padding: '10px 12px', background: row.change ? 'color-mix(in srgb, var(--jp-accent) 15%, var(--jp-surface))' : undefined, borderLeft: row.change ? `3px solid ${ACCENT}` : '3px solid transparent' }, children: [_jsx("dt", { style: { color: DIMMED, minWidth: 80 }, children: row.label }), _jsxs("dd", { style: { margin: 0, overflowWrap: 'anywhere', flex: 1 }, children: [displayData(row.value), row.change ? _jsxs("div", { style: { color: INK2, fontSize: 13, marginTop: 4 }, children: [_jsx("span", { children: displayData(row.change.before) }), " \u2192 ", _jsx("span", { children: displayData(row.change.after) })] }) : null] })] }, `${row.key}:${i}`)) }) : _jsx("div", { style: { color: DIMMED }, children: i18n.stage.noData })] });
974
+ }
975
+ function StateFlow({ node, p }) {
976
+ const { t: i18n } = useI18n();
977
+ const { changed = [] } = useCtx();
978
+ const items = stateFlowItems(p.value, p.states, p.labels);
979
+ const change = changeFor(node.props?.value, changed);
980
+ const label = (v) => items.find(item => item.value === v)?.label ?? displayData(v);
981
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 10 } });
982
+ return _jsxs("div", { ...root, children: [_jsx("ol", { style: { listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10 }, children: items.map((item, i) => _jsxs("li", { "aria-current": item.state === 'current' ? 'step' : undefined, style: { display: 'flex', gap: 10, alignItems: 'center' }, children: [i ? _jsx("span", { "aria-hidden": true, style: { color: DIMMED }, children: "\u2192" }) : null, _jsxs("span", { className: "jbox", style: { display: 'inline-flex', gap: 6, alignItems: 'center', padding: '8px 14px', color: item.state === 'todo' ? DIMMED : INK, background: item.state === 'current' ? 'color-mix(in srgb, var(--jp-accent) 20%, var(--jp-surface))' : SURFACE, fontWeight: item.state === 'current' ? 600 : 400 }, children: [_jsx(Rough, { seedKey: `${node.id}:${i}`, stroke: item.state === 'current' ? ACCENT : item.state === 'done' ? OK : LINE }), item.state === 'done' ? _jsx("span", { "aria-label": i18n.stage.done, style: { color: OK }, children: "\u2713" }) : null, item.label] })] }, `${item.value}:${i}`)) }), change ? _jsx("div", { "data-state-change": "", style: { color: INK2, fontSize: 14 }, children: i18n.stage.stateChange(label(change.before), label(change.after)) }) : null, !items.length ? _jsx("span", { style: { color: DIMMED }, children: i18n.stage.noStates }) : null] });
983
+ }
984
+ const ARTIFACT_ICON = { doc: '▤', code: '</>', sheet: '▦', web: '◎', image: '▣' };
985
+ const ARTIFACT_KIND = (i18n) => ({ doc: i18n.stage.document, code: i18n.stage.code, sheet: i18n.stage.sheet, web: i18n.stage.webPage, image: i18n.stage.image });
986
+ function ArtifactStatus({ status }) {
987
+ const { t: i18n } = useI18n();
988
+ if (status === 'generating')
989
+ return (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 6, color: INFO, flexShrink: 0, fontSize: 13 }, children: [_jsx(Spinner, { size: 13, c: INFO }), i18n.stage.generating] }));
990
+ if (status === 'error')
991
+ return _jsx("span", { style: { color: DANGER, flexShrink: 0, fontSize: 13 }, children: i18n.stage.generationFailed });
992
+ return _jsx("span", { style: { color: OK, flexShrink: 0, fontSize: 13 }, children: i18n.stage.generated });
993
+ }
994
+ /** Agent 的侧边产物(D56):标题栏(类型图标、标题、版本、状态)+ 内容区;默认占满所在容器的高度。 */
995
+ function Artifact({ node, p, children }) {
996
+ const { t: i18n } = useI18n();
997
+ const mode = useStroke();
998
+ const kind = str(p.kind, 'doc');
999
+ const root = useRoot(node, p, { className: cn('jbox', mode === 'clean' && 'rounded-xl'), style: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%', minHeight: 0, background: SURFACE, overflow: 'hidden' } });
1000
+ return (_jsxs("div", { ...root, "data-kind": kind, children: [_jsx(Rough, { seedKey: node.id }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 10, padding: '12px 16px', borderBottom: `1px solid ${LINE}`, minWidth: 0 }, children: [_jsx("span", { "aria-hidden": true, style: { fontFamily: MONO, fontSize: 13, color: INK2, flexShrink: 0 }, children: ARTIFACT_ICON[kind] ?? ARTIFACT_ICON.doc }), _jsx("span", { className: "jtrunc", style: { fontWeight: 600, color: INK }, children: str(p.title, i18n.stage.artifact) }), p.version !== undefined ? _jsx("span", { style: { flexShrink: 0, fontSize: 12, color: DIMMED, border: `1px solid ${LINE_STRONG}`, borderRadius: 999, padding: '0 7px', lineHeight: '18px' }, children: str(p.version) }) : null, _jsx("span", { style: { flex: 1 } }), _jsx(ArtifactStatus, { status: str(p.status, 'ready') })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 10, whiteSpace: 'pre-wrap', lineHeight: 1.6 }, children: children })] }));
1001
+ }
1002
+ /** 对话里指向产物的卡片(可点击打开)。 */
1003
+ function ArtifactRef({ node, p, disabled }) {
1004
+ const { t: i18n } = useI18n();
1005
+ const mode = useStroke();
1006
+ const kind = str(p.kind, 'doc');
1007
+ const root = useRoot(node, p, { disabled, className: cn('jbox', mode === 'clean' && 'rounded-lg'), style: { display: 'flex', alignItems: 'center', gap: 12, width: '100%', maxWidth: 360, padding: '10px 14px', background: SURFACE, textAlign: 'left' } });
1008
+ return (_jsxs("button", { ...root, type: "button", "data-jcontrol": node.id, children: [_jsx(Rough, { seedKey: node.id }), _jsx("span", { "aria-hidden": true, style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, flexShrink: 0, borderRadius: 8, background: SUBTLE, fontFamily: MONO, fontSize: 13, color: INK2 }, children: ARTIFACT_ICON[kind] ?? ARTIFACT_ICON.doc }), _jsxs("span", { style: { display: 'flex', flexDirection: 'column', minWidth: 0, flex: 1 }, children: [_jsx("span", { className: "jtrunc", style: { fontWeight: 600, color: INK }, children: str(p.title, i18n.stage.artifact) }), _jsx("span", { style: { fontSize: 12, color: DIMMED }, children: i18n.stage.artifactSummary(ARTIFACT_KIND(i18n)[kind] ?? i18n.stage.document, p.status === 'generating') })] })] }));
1009
+ }
1010
+ function PromptInput({ node, p, disabled }) {
1011
+ const { t: i18n } = useI18n();
1012
+ const ctx = useCtx();
1013
+ const v = useInputValue(node, p);
1014
+ const has = v !== undefined && v !== null && v !== '';
1015
+ const root = useRoot(node, p, { disabled, className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-xl', style: { flex: 1, minWidth: 0, width: '100%', minHeight: 44, padding: '10px 14px', whiteSpace: 'pre-wrap', color: has ? INK : SOFT, background: SURFACE } });
1016
+ return (_jsxs("div", { ...root, "data-jcontrol": node.id, children: [_jsx(Rough, { seedKey: node.id }), _jsx("textarea", { readOnly: !ctx.interactive || disabled, tabIndex: ctx.interactive && !disabled ? 0 : -1, onChange: e => ctx.act?.(node.id, { type: 'type', value: e.currentTarget.value }), onClick: ctx.interactive ? e => e.stopPropagation() : undefined, "aria-label": nodeLabel(node), value: str(v), placeholder: str(p.placeholder, i18n.stage.typeAMessage), rows: 1, className: FIELD_CLASS, style: { display: 'block', width: '100%', font: 'inherit', color: INK, lineHeight: '24px', maxHeight: 144, overflowY: 'auto' } })] }));
1017
+ }
1018
+ // ============ 设备外框:终端与手机 ============
1019
+ const TERM_TONE = { plain: INK, ok: OK, warn: WARN, error: DANGER };
1020
+ const TERM_MONO = '[.jstage[data-stroke=clean]_&]:font-mono';
1021
+ /**
1022
+ * 终端(低保真线框,与其他组件同一风格):圆角窗口,左上角是 title;里面每条命令、每段输出各是一个带边框的圆角块。
1023
+ * 内容超出 h / mah 时在窗口内部滚动。tone 只改输出文字的颜色。
1024
+ */
1025
+ function Terminal({ node, p, children }) {
1026
+ // 像真实终端一样停在最新的输出(输入行可见)
1027
+ const body = useRef(null);
1028
+ useLayoutEffect(() => {
1029
+ const el = body.current;
1030
+ if (el)
1031
+ el.scrollTop = el.scrollHeight;
1032
+ });
1033
+ const root = useRoot(node, p, {
1034
+ className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-[18px]',
1035
+ style: { display: 'flex', flexDirection: 'column', gap: 10, width: '100%', minHeight: 0, background: SURFACE, padding: '12px 16px 16px' },
1036
+ });
1037
+ return (_jsxs("div", { ...root, children: [_jsx(Rough, { seedKey: node.id, shape: "roundrect", radius: 18, strokeWidth: 1.5 }), _jsx("div", { className: "jtrunc", style: { flexShrink: 0, fontSize: 15 }, children: str(p.title, 'Terminal') }), _jsx("div", { ref: body, style: { flex: 1, minHeight: 0, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 12, padding: 2 }, "data-jterm-body": "", children: children })] }));
1038
+ }
1039
+ function TerminalCommand({ node, p, children }) {
1040
+ const root = useRoot(node, p, {
1041
+ className: cn('jbox [.jstage[data-stroke=clean]_&]:rounded-[10px]', TERM_MONO),
1042
+ style: { display: 'flex', gap: 8, padding: '10px 16px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', background: SURFACE, flexShrink: 0 },
1043
+ });
1044
+ return (_jsxs("div", { ...root, children: [_jsx(Rough, { seedKey: node.id, shape: "roundrect", radius: 10 }), _jsx("span", { "aria-hidden": true, style: { flexShrink: 0 }, children: str(p.prompt, '$') }), _jsx("span", { style: { minWidth: 0 }, children: children })] }));
1045
+ }
1046
+ function TerminalOutput({ node, p, children }) {
1047
+ const tone = str(p.tone, 'plain');
1048
+ const root = useRoot(node, p, {
1049
+ className: cn('jbox [.jstage[data-stroke=clean]_&]:rounded-[14px]', TERM_MONO),
1050
+ style: { padding: '10px 16px', minHeight: 56, whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: 1.55, background: SURFACE, color: TERM_TONE[tone] ?? INK, flexShrink: 0 },
1051
+ });
1052
+ return (_jsxs("div", { ...root, "data-tone": tone in TERM_TONE ? tone : 'plain', children: [_jsx(Rough, { seedKey: node.id, shape: "roundrect", radius: 14 }), _jsx("div", { children: children })] }));
1053
+ }
1054
+ /** 终端里的输入行(值类型):与命令块同样的圆角块,type 动作写入;没有值时显示 placeholder 与光标。 */
1055
+ function TerminalInput({ node, p, disabled }) {
1056
+ const v = useInputValue(node, p);
1057
+ const has = v !== undefined && v !== null && v !== '';
1058
+ const root = useRoot(node, p, {
1059
+ disabled,
1060
+ className: cn('jbox [.jstage[data-stroke=clean]_&]:rounded-[10px]', TERM_MONO),
1061
+ style: { display: 'flex', gap: 8, alignItems: 'baseline', padding: '10px 16px', background: SURFACE, flexShrink: 0 },
1062
+ });
1063
+ return (_jsxs("div", { ...root, "data-soft": "", "data-dashed": "", children: [_jsx(Rough, { seedKey: node.id, shape: "roundrect", radius: 10, stroke: SOFT, dashed: true }), _jsx("span", { "aria-hidden": true, style: { flexShrink: 0 }, children: str(p.prompt, '$') }), _jsxs("span", { role: "textbox", "aria-readonly": "true", tabIndex: 0, "data-jcontrol": node.id, style: { minWidth: 0, flex: 1, whiteSpace: 'pre-wrap', color: has ? INK : SOFT }, children: [_jsx("span", { "data-jtext": "", children: has ? str(v) : str(p.placeholder) }), _jsx("span", { "aria-hidden": true, style: { display: 'inline-block', width: 2, height: '1.1em', marginLeft: 2, verticalAlign: 'text-bottom', background: INK } })] })] }));
1064
+ }
1065
+ /** 手机外框的几何尺寸:机身边框,以及屏幕内的安全区(顶部避开听筒与圆角、底部避开圆角与横条、左右避开圆角)。 */
1066
+ export const PHONE_BEZEL = 12;
1067
+ export const PHONE_SAFE = { top: 40, bottom: 26, x: 12 };
1068
+ const PHONE_RADIUS = 54;
1069
+ const PHONE_BUTTONS = [
1070
+ { side: 'left', top: 118, h: 30 },
1071
+ { side: 'left', top: 176, h: 58 },
1072
+ { side: 'left', top: 246, h: 58 },
1073
+ { side: 'right', top: 196, h: 90 },
1074
+ ];
1075
+ /** 横放(刘海在左)时屏幕内的安全区:左侧避开刘海,右侧与上下避开圆角,底部避开横条。 */
1076
+ export const PHONE_SAFE_LANDSCAPE = { top: 12, bottom: 20, left: 44, right: 28 };
1077
+ const phoneInsets = (landscape) => landscape ? PHONE_SAFE_LANDSCAPE : { top: PHONE_SAFE.top, bottom: PHONE_SAFE.bottom, left: PHONE_SAFE.x, right: PHONE_SAFE.x };
1078
+ /** 外框占用的总尺寸(含侧键余量),内容区为 width × height;横放时侧键在机身上下沿。 */
1079
+ export function phoneFrameSize(width, height, landscape = false) {
1080
+ const s = phoneInsets(landscape);
1081
+ const screenW = width + s.left + s.right;
1082
+ const screenH = height + s.top + s.bottom;
1083
+ return landscape ? { width: screenW + PHONE_BEZEL * 2, height: screenH + PHONE_BEZEL * 2 + 8 } : { width: screenW + PHONE_BEZEL * 2 + 8, height: screenH + PHONE_BEZEL * 2 };
1084
+ }
1085
+ /**
1086
+ * 手机外框(Phone 组件与舞台的自动外框共用):大圆角机身、侧边按键、顶部听筒、底部横条,不显示任何文字。
1087
+ * children 放在屏幕安全区以内的内容区,内容区正好是 width × height(断点按 width 计算,不被圆角和听筒遮挡);
1088
+ * scale 只做视觉缩放。主题取外层 ThemeContext(sketch 用手绘描边)。
1089
+ */
1090
+ export function PhoneFrame({ width, height, scale = 1, children, seed = 'phone', landscape = false }) {
1091
+ const mode = useStroke();
1092
+ const { theme, font, style: fontStyle } = useLook();
1093
+ const sketch = mode === 'rough';
1094
+ const safe = phoneInsets(landscape);
1095
+ const screenW = width + safe.left + safe.right;
1096
+ const screenH = height + safe.top + safe.bottom;
1097
+ const W = screenW + PHONE_BEZEL * 2;
1098
+ const H = screenH + PHONE_BEZEL * 2;
1099
+ const ink = INK;
1100
+ const size = phoneFrameSize(width, height, landscape);
1101
+ // 竖放:侧键在左右两侧,机身左移 4 留出左侧键;横放(逆时针转 90°):左侧键转到下沿、右侧键转到上沿,机身下移 4
1102
+ const buttons = PHONE_BUTTONS.map((b) => landscape
1103
+ ? { edge: (b.side === 'left' ? 'bottom' : 'top'), at: b.side === 'left' ? b.top : W - b.top - b.h, len: b.h }
1104
+ : { edge: b.side, at: b.top, len: b.h });
1105
+ return (_jsx("div", { className: "jtheme", style: { width: size.width * scale, height: size.height * scale, position: 'relative', flexShrink: 0, ...fontStyle }, "data-jphone-frame": "", "data-theme": theme, "data-stroke": mode, "data-font": font ?? undefined, children: _jsxs("div", { style: { position: 'absolute', left: landscape ? 0 : 4, top: landscape ? 4 : 0, width: W, height: H, transform: scale === 1 ? undefined : `scale(${scale})`, transformOrigin: '0 0' }, children: [buttons.map((b, i) => (_jsx("div", { "aria-hidden": true, "data-jphone-button": b.edge, style: b.edge === 'left' || b.edge === 'right'
1106
+ ? { position: 'absolute', top: b.at, height: b.len, width: 5, [b.edge]: -5, background: sketch ? 'transparent' : ink, borderRadius: b.edge === 'left' ? '3px 0 0 3px' : '0 3px 3px 0' }
1107
+ : { position: 'absolute', left: b.at, width: b.len, height: 5, [b.edge]: -5, background: sketch ? 'transparent' : ink, borderRadius: b.edge === 'top' ? '3px 3px 0 0' : '0 0 3px 3px' }, children: sketch ? _jsx(Rough, { seedKey: `${seed}:btn${i}`, strokeWidth: 1.4, inset: 0.5 }) : null }, i))), _jsx("div", { style: { position: 'absolute', inset: 0, borderRadius: PHONE_RADIUS, background: sketch ? SURFACE : ink, boxShadow: sketch ? 'none' : '0 18px 40px rgba(0,0,0,0.18)' }, children: sketch ? _jsx(Rough, { seedKey: `${seed}:body`, shape: "roundrect", radius: PHONE_RADIUS, strokeWidth: 2, roughness: 0.8, inset: 1.5 }) : null }), _jsxs("div", { style: { position: 'absolute', left: PHONE_BEZEL, top: PHONE_BEZEL, width: screenW, height: screenH, borderRadius: PHONE_RADIUS - PHONE_BEZEL, overflow: 'hidden', background: SURFACE, isolation: 'isolate' }, "data-jphone-screen": "", children: [sketch ? null : landscape
1108
+ ? _jsx("div", { "aria-hidden": true, style: { position: 'absolute', left: 12, top: '50%', width: 24, height: 96, marginTop: -48, borderRadius: 12, background: ink } })
1109
+ : _jsx("div", { "aria-hidden": true, style: { position: 'absolute', top: 12, left: '50%', width: 96, height: 24, marginLeft: -48, borderRadius: 12, background: ink } }), _jsx("div", { "aria-hidden": true, style: { position: 'absolute', bottom: landscape ? 7 : 9, left: '50%', width: landscape ? 160 : 120, height: 4, marginLeft: landscape ? -80 : -60, borderRadius: 2, background: sketch ? INK2 : LINE_STRONG } }), _jsx("div", { style: { position: 'absolute', left: safe.left, top: safe.top, width, height, overflow: 'hidden' }, "data-jphone-content-box": "", children: children })] }), sketch ? (landscape
1110
+ ? _jsx("div", { "aria-hidden": true, style: { position: 'absolute', left: PHONE_BEZEL + 14, top: '50%', width: 6, height: 64, marginTop: -32, borderRadius: 3, border: `1.5px solid ${INK2}` } })
1111
+ : _jsx("div", { "aria-hidden": true, style: { position: 'absolute', top: PHONE_BEZEL + 14, left: '50%', width: 64, height: 6, marginLeft: -32, borderRadius: 3, border: `1.5px solid ${INK2}` } })) : null] }) }));
1112
+ }
1113
+ /** Phone 组件:子节点放进手机屏幕,按手机宽度排版(响应式断点按这个宽度计算);弹窗渲染在屏幕内。 */
1114
+ function Phone({ node, p, children }) {
1115
+ const ctx = useCtx();
1116
+ const [layer, setLayer] = useState(null);
1117
+ const r = useR(p);
1118
+ const landscape = p.landscape === true;
1119
+ // 横放:内容区按横屏手机的尺寸排版(断点按这个宽度算);h 仍可改高度
1120
+ const width = childWidth(node, ctx.width, p);
1121
+ const hv = Number(r('h'));
1122
+ const height = Number.isFinite(hv) && hv > 0 ? hv : landscape ? PHONE_LANDSCAPE.height : VIEWPORT_SIZE.mobile.height;
1123
+ const sv = Number(r('scale'));
1124
+ const scale = Number.isFinite(sv) && sv > 0 ? sv : 1;
1125
+ const { h: _h, w: _w, landscape: _l, ...rest } = p;
1126
+ const root = useRoot(node, rest, { style: { display: 'inline-block', flexShrink: 0 } });
1127
+ return (_jsx("div", { ...root, children: _jsxs(PhoneFrame, { width: width, height: height, scale: scale, seed: node.id, landscape: landscape, children: [_jsx(RendererContextProvider, { value: { width, dialogLayer: layer }, children: _jsx("div", { style: { position: 'absolute', inset: 0, overflow: 'auto' }, "data-jphone-content": "", children: children }) }), _jsx("div", { ref: setLayer, className: "pointer-events-none absolute inset-0 z-20 [&>*]:pointer-events-auto", "data-jlayer": "dialog" })] }) }));
1128
+ }
1129
+ // ============ Joplo ============
1130
+ function Placeholder({ node, p }) {
1131
+ const { t: i18n } = useI18n();
1132
+ const h = dim(p.h ?? p.height) ?? '120px';
1133
+ const root = useRoot(node, p, { className: 'jbox', style: { display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'center', background: SUBTLE, fontSize: 14, color: DIMMED, height: h } });
1134
+ return (_jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT, fill: LINE, fillStyle: "cross-hatch" }), _jsx("span", { className: "rounded bg-[color-mix(in_srgb,var(--jp-surface)_90%,transparent)] px-2", children: str(p.label, i18n.stage.placeholder) })] }));
1135
+ }
1136
+ // ============ 产品界面(D68):官网示例里反复手拼的常见界面 ============
1137
+ // 都只读主题变量(--jp-*)与 Rough 描边;状态全部来自绑定的属性 / state.inputs,由步骤驱动;不请求网络、不加载外部资源。
1138
+ const ROW_GAP = 12;
1139
+ /** Avatar.Group:头像叠在一起(负间距 + 底色描边圈)。 */
1140
+ function AvatarGroup({ node, p, children }) {
1141
+ const overlap = typeof p.spacing === 'number' ? p.spacing : (SPACING[str(p.spacing, 'sm')] ?? 12) * 0.7;
1142
+ const root = useRoot(node, p, { className: 'javatar-group', style: { display: 'inline-flex', alignItems: 'center', ['--javatar-overlap']: `${-Math.round(overlap)}px` } });
1143
+ return _jsx("div", { ...root, children: children });
1144
+ }
1145
+ /** Radio.Card:Radio.Group 里的可选卡片;是否选中读 Radio.Group 的当前值。 */
1146
+ function RadioCard({ node, p, disabled }) {
1147
+ const host = useCtx();
1148
+ const ctx = useContext(ValueContext);
1149
+ const standalone = useInputValue(node, p) === true;
1150
+ const on = ctx ? ctx.value !== null && ctx.value === str(p.value) : standalone;
1151
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { display: 'flex', alignItems: 'flex-start', gap: 12, width: '100%', padding: '12px 14px', background: on ? tint(INK, 6) : SURFACE } });
1152
+ return (_jsxs("div", { ...root, role: "radio", tabIndex: host.interactive ? 0 : undefined, onClick: host.interactive ? e => { e.stopPropagation(); if (!disabled && ctx && !ctx.disabled)
1153
+ host.act?.(node.id, { type: 'click' }, { type: 'select', target: ctx.owner, value: str(p.value) }); } : undefined, onKeyDown: host.interactive ? e => { if (e.key === ' ' || e.key === 'Enter') {
1154
+ e.preventDefault();
1155
+ e.stopPropagation();
1156
+ e.currentTarget.click();
1157
+ } } : undefined, "aria-checked": on, "data-jchecked": on ? '' : undefined, children: [_jsx(Rough, { seedKey: node.id, stroke: on ? INK : SOFT, strokeWidth: on ? 1.8 : 1.1 }), p.leftSection !== undefined ? _jsx("span", { "aria-hidden": true, style: { fontSize: 20, lineHeight: '24px' }, children: str(p.leftSection) }) : null, _jsxs("span", { style: { display: 'flex', flexDirection: 'column', gap: 2, flex: 1, minWidth: 0 }, children: [p.label !== undefined ? _jsx("span", { style: { fontWeight: on ? 650 : 500 }, children: str(p.label) }) : null, p.description !== undefined ? _jsx("span", { style: { fontSize: 13, color: DIMMED, lineHeight: 1.45 }, children: str(p.description) }) : null] }), _jsxs("span", { className: "jbox inline-flex shrink-0 items-center justify-center rounded-full", style: { width: 20, height: 20, marginTop: 2 }, children: [_jsx(Rough, { seedKey: `${node.id}:dot`, shape: "ellipse", inset: 1 }), on ? _jsx("span", { style: { width: 9, height: 9, borderRadius: 5, background: INK } }) : null] })] }));
1158
+ }
1159
+ /** EmptyState:空列表、404 / 过期 / 无权限页、结果页。图标圈 + 标题 + 说明 + 子节点里的操作按钮。 */
1160
+ function EmptyState({ node, p, children }) {
1161
+ const c = color(p.color) ?? INK2;
1162
+ const border = p.withBorder === true;
1163
+ const root = useRoot(node, p, { className: border ? 'jbox [.jstage[data-stroke=clean]_&]:rounded-lg' : undefined, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10, textAlign: 'center', width: '100%', padding: '32px 24px' } });
1164
+ return (_jsxs("div", { ...root, role: "status", "data-dashed": border ? '' : undefined, children: [border ? _jsx(Rough, { seedKey: node.id, dashed: true, stroke: SOFT }) : null, p.icon !== undefined ? (_jsxs("span", { "aria-hidden": true, className: "jbox", "data-jround": "", style: { width: 56, height: 56, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: 999, background: light(c), color: c, fontSize: 24, marginBottom: 4 }, children: [_jsx(Rough, { seedKey: `${node.id}:icon`, shape: "ellipse", stroke: c, strokeWidth: 1.1 }), _jsx("span", { children: str(p.icon) })] })) : null, p.title !== undefined ? _jsx("div", { style: { fontSize: 18, fontWeight: 700, lineHeight: 1.35 }, children: str(p.title) }) : null, p.description !== undefined ? _jsx("div", { style: { fontSize: 14, color: DIMMED, lineHeight: 1.55, maxWidth: 420, whiteSpace: 'pre-line' }, children: str(p.description) }) : null, children ? _jsx("div", { style: { display: 'flex', gap: 10, flexWrap: 'wrap', justifyContent: 'center', marginTop: 6 }, children: children }) : null] }));
1165
+ }
1166
+ /** ListRow:头像 / 图标 + 标题 + 副标题,右侧内容放在子节点。active 时浅色底。 */
1167
+ function ListRow({ node, p, children }) {
1168
+ const c = color(p.color) ?? INK2;
1169
+ const active = p.active === true;
1170
+ const border = p.withBorder === true;
1171
+ const root = useRoot(node, p, { className: border ? 'jbox [.jstage[data-stroke=clean]_&]:rounded-md' : 'jlistrow', style: { display: 'flex', alignItems: 'center', gap: ROW_GAP, width: '100%', padding: border ? '10px 12px' : '10px 4px', background: active ? tint(INK, 7) : border ? SURFACE : undefined, borderRadius: border ? undefined : 8 } });
1172
+ return (_jsxs("div", { ...root, "data-jactive": active ? '' : undefined, children: [border ? _jsx(Rough, { seedKey: node.id, stroke: active ? INK : SOFT, strokeWidth: active ? 1.6 : 1.1 }) : null, p.avatar !== undefined ? (_jsxs("span", { className: "jbox", "data-jround": "", "aria-hidden": true, style: { width: 38, height: 38, borderRadius: 999, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, color: c, background: light(c), flexShrink: 0, fontWeight: 600 }, children: [_jsx(Rough, { seedKey: `${node.id}:avatar`, shape: "ellipse", stroke: c, strokeWidth: 1.1 }), _jsx("span", { children: initials(str(p.avatar)) })] })) : p.icon !== undefined ? (_jsx("span", { "aria-hidden": true, style: { width: 38, height: 38, borderRadius: 10, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, background: SUBTLE, flexShrink: 0 }, children: str(p.icon) })) : null, _jsxs("span", { style: { display: 'flex', flexDirection: 'column', gap: 2, flex: 1, minWidth: 0 }, children: [p.title !== undefined ? _jsx("span", { className: "jtrunc", style: { fontWeight: 600, lineHeight: 1.35 }, children: str(p.title) }) : null, p.description !== undefined ? _jsx("span", { className: "jtrunc", style: { fontSize: 13, color: DIMMED, lineHeight: 1.4 }, children: str(p.description) }) : null] }), children ? _jsx("span", { style: { display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }, children: children }) : null] }));
1173
+ }
1174
+ /** diff 的颜色:以 - / − 开头为红,以 + 开头为绿,其余跟随 color。 */
1175
+ export function diffTone(diff) {
1176
+ const t = diff.trim();
1177
+ if (/^[-−]/.test(t))
1178
+ return 'down';
1179
+ if (/^\+/.test(t))
1180
+ return 'up';
1181
+ return 'flat';
1182
+ }
1183
+ /** Stat:指标卡。 */
1184
+ function Stat({ node, p }) {
1185
+ const c = color(p.color) ?? INK;
1186
+ const diff = p.diff !== undefined && p.diff !== null && p.diff !== '' ? str(p.diff) : null;
1187
+ const tone = diff ? diffTone(diff) : 'flat';
1188
+ const progress = typeof p.progress === 'number' && Number.isFinite(p.progress) ? Math.max(0, Math.min(100, p.progress)) : null;
1189
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-lg', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '14px 16px', background: SURFACE, minWidth: 0 } });
1190
+ return (_jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, color: DIMMED, fontSize: 13 }, children: [p.icon !== undefined ? _jsx("span", { "aria-hidden": true, children: str(p.icon) }) : null, _jsx("span", { className: "jtrunc", children: str(p.label) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }, children: [_jsx("span", { style: { fontSize: 28, fontWeight: 700, lineHeight: 1.1, color: c }, "data-jvalue": "", children: str(p.value) }), diff ? _jsx("span", { style: { fontSize: 13, fontWeight: 600, color: tone === 'down' ? DANGER : tone === 'up' ? OK : DIMMED }, "data-jdiff": tone, children: diff }) : null] }), p.description !== undefined ? _jsx("div", { style: { fontSize: 12, color: DIMMED }, children: str(p.description) }) : null, progress !== null ? (_jsx("div", { style: { height: 6, background: SUBTLE, borderRadius: 999, overflow: 'hidden', marginTop: 2 }, role: "progressbar", "aria-valuenow": progress, children: _jsx("div", { style: { width: `${progress}%`, height: '100%', background: c } }) })) : null] }));
1191
+ }
1192
+ /** KeyValue:一列“名称 …… 值”;total 时最后一行加粗并在上方画分隔线。 */
1193
+ function KeyValue({ node, p }) {
1194
+ const items = arr(p.items).filter((x) => !!x && typeof x === 'object');
1195
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', width: '100%' } });
1196
+ return (_jsx("dl", { ...root, style: { ...root.style, margin: 0 }, children: items.map((it, i) => {
1197
+ const last = i === items.length - 1;
1198
+ const total = p.total === true && last && items.length > 1;
1199
+ return (_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 16, padding: '7px 0', borderTop: total || (p.withDivider === true && i > 0) ? `1px solid ${LINE}` : undefined, fontWeight: total ? 700 : undefined, fontSize: total ? 16 : 14 }, "data-jtotal": total ? '' : undefined, children: [_jsx("dt", { style: { color: total ? INK : DIMMED, minWidth: 0 }, children: str(it.label) }), _jsx("dd", { style: { margin: 0, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }, children: str(it.value) })] }, i));
1200
+ }) }));
1201
+ }
1202
+ // ---------- 图表(BarChart / LineChart,对齐 @mantine/charts 的 data / dataKey / series) ----------
1203
+ const SERIES_COLORS = [INFO, ACCENT, OK, DANGER, '#7048e8', '#0ca678'];
1204
+ /** 图表的纯数据部分:每个系列取 data[i][name](不是数字的当作 0),刻度取“好看”的整数步长。 */
1205
+ export function chartModel(data, dataKey, series) {
1206
+ const rows = arr(data).filter((r) => !!r && typeof r === 'object');
1207
+ const key = str(dataKey);
1208
+ const list = arr(series)
1209
+ .filter((s) => !!s && typeof s === 'object' && typeof s.name === 'string')
1210
+ .map((s, i) => ({ name: String(s.name), label: s.label !== undefined ? str(s.label) : String(s.name), color: color(s.color) ?? SERIES_COLORS[i % SERIES_COLORS.length], values: rows.map((r) => (typeof r[String(s.name)] === 'number' && Number.isFinite(r[String(s.name)]) ? r[String(s.name)] : 0)) }));
1211
+ const peak = Math.max(0, ...list.flatMap((s) => s.values));
1212
+ const step = niceStep(peak / 4);
1213
+ const max = Math.max(step, Math.ceil(peak / step) * step);
1214
+ const ticks = Array.from({ length: Math.round(max / step) + 1 }, (_, i) => i * step);
1215
+ return { labels: rows.map((r) => str(r[key])), series: list, max, ticks };
1216
+ }
1217
+ function niceStep(raw) {
1218
+ if (!(raw > 0))
1219
+ return 1;
1220
+ const pow = 10 ** Math.floor(Math.log10(raw));
1221
+ const f = raw / pow;
1222
+ return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * pow;
1223
+ }
1224
+ function Chart({ node, p, kind }) {
1225
+ const { animate } = useCtx();
1226
+ const r = useR(p);
1227
+ const m = chartModel(p.data, p.dataKey, p.series);
1228
+ const H = typeof r('h') === 'number' ? r('h') : 220;
1229
+ const W = 600;
1230
+ const pad = { l: 40, r: 12, t: 12, b: 28 };
1231
+ const iw = W - pad.l - pad.r;
1232
+ const legend = (m.series.length > 1 && p.withLegend !== false) || p.withLegend === true;
1233
+ // h 是整张图(含图例)的高度:图例占 22px,其余给 SVG
1234
+ const svgH = Math.max(80, H - (legend ? 22 : 0));
1235
+ const ih = svgH - pad.t - pad.b;
1236
+ const unit = p.unit !== undefined ? str(p.unit) : '';
1237
+ const n = Math.max(1, m.labels.length);
1238
+ const band = iw / n;
1239
+ const y = (v) => pad.t + ih - (v / m.max) * ih;
1240
+ const root = useRoot(node, p, { style: { width: '100%', position: 'relative' } });
1241
+ return (_jsxs("div", { ...root, style: { ...root.style, height: 'auto', minHeight: H }, role: "img", "aria-label": m.series.map((s) => s.label).join(', '), children: [_jsxs("svg", { viewBox: `0 0 ${W} ${svgH}`, width: "100%", height: svgH, preserveAspectRatio: "none", style: { display: 'block', overflow: 'visible' }, children: [m.ticks.map((t) => (_jsxs("g", { children: [_jsx("line", { x1: pad.l, x2: W - pad.r, y1: y(t), y2: y(t), style: { stroke: LINE }, strokeDasharray: t === 0 ? undefined : '3 4' }), _jsx("text", { x: pad.l - 6, y: y(t) + 4, textAnchor: "end", style: { fill: DIMMED, fontSize: 11 }, children: `${t}${unit}` })] }, t))), m.labels.map((l, i) => (_jsx("text", { x: pad.l + band * (i + 0.5), y: pad.t + ih + 18, textAnchor: "middle", style: { fill: DIMMED, fontSize: 11 }, children: l }, i))), kind === 'bar'
1242
+ ? m.series.map((s, si) => {
1243
+ const bw = Math.max(4, (band * 0.7) / m.series.length);
1244
+ return s.values.map((v, i) => {
1245
+ const x = pad.l + band * i + band * 0.15 + bw * si;
1246
+ return _jsx(motion.rect, { x: x, width: bw - 2, rx: 2, style: { fill: s.color }, initial: false, animate: { y: y(v), height: Math.max(0, pad.t + ih - y(v)) }, transition: animate ? { duration: 0.5, ease: [0.16, 1, 0.3, 1] } : { duration: 0 }, "data-jbar": `${s.name}:${i}` }, `${si}:${i}`);
1247
+ });
1248
+ })
1249
+ : m.series.map((s, si) => {
1250
+ const pts = s.values.map((v, i) => [pad.l + band * (i + 0.5), y(v)]);
1251
+ const d = pts.map(([x, yy], i) => `${i ? 'L' : 'M'}${x.toFixed(1)},${yy.toFixed(1)}`).join(' ');
1252
+ return (_jsxs("g", { children: [_jsx(motion.path, { d: d, fill: "none", style: { stroke: s.color }, strokeWidth: 2.2, strokeLinejoin: "round", initial: false, animate: { d }, transition: animate ? { duration: 0.5 } : { duration: 0 } }), pts.map(([x, yy], i) => _jsx(motion.circle, { r: 3.5, cx: x, style: { fill: SURFACE, stroke: s.color }, strokeWidth: 2, initial: false, animate: { cy: yy }, transition: animate ? { duration: 0.5 } : { duration: 0 }, "data-jpoint": `${s.name}:${i}` }, i))] }, si));
1253
+ })] }), legend ? (_jsx("div", { style: { display: 'flex', gap: 14, justifyContent: 'center', alignItems: 'center', height: 22, fontSize: 12, color: DIMMED }, children: m.series.map((s) => (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 6 }, children: [_jsx("span", { "aria-hidden": true, style: { width: 10, height: 10, borderRadius: 2, background: s.color } }), s.label] }, s.name))) })) : null] }));
1254
+ }
1255
+ const BarChart = (props) => _jsx(Chart, { ...props, kind: "bar" });
1256
+ const LineChart = (props) => _jsx(Chart, { ...props, kind: "line" });
1257
+ // ---------- Menu(对齐 Mantine):opened 写在 Menu 上,编译器抄给 Menu.Dropdown(core 门控) ----------
1258
+ const MenuContext = createContext(null);
1259
+ function Menu({ node, p, children }) {
1260
+ const anchor = useRef(null);
1261
+ const root = useRoot(node, p, { style: { position: 'relative', display: 'inline-block' } });
1262
+ return (_jsx(MenuContext.Provider, { value: { anchor, position: str(p.position, 'bottom-start') }, children: _jsx("div", { ...root, ref: anchor, children: children }) }));
1263
+ }
1264
+ function MenuTarget({ node, p, children }) {
1265
+ const root = useRoot(node, p, { style: { display: 'inline-flex' } });
1266
+ return _jsx("span", { ...root, children: children });
1267
+ }
1268
+ /** 下拉相对 Menu 的位置:top-* 在上方,*-end 右对齐。 */
1269
+ export function menuPlacement(position) {
1270
+ return { top: position.startsWith('top'), end: position.endsWith('end') };
1271
+ }
1272
+ function MenuDropdown({ node, p, children }) {
1273
+ const { dialogLayer, animate } = useCtx();
1274
+ const menu = useContext(MenuContext);
1275
+ const place = menuPlacement(menu?.position ?? 'bottom-start');
1276
+ const [pos, setPos] = useState(null);
1277
+ // 浮到弹窗层(与 Modal 同一层,跟着镜头一起平移缩放),不被表格、卡片的 overflow 裁掉;坐标按镜头缩放换回画面坐标
1278
+ useLayoutEffect(() => {
1279
+ const a = menu?.anchor.current;
1280
+ if (!a || !dialogLayer)
1281
+ return;
1282
+ const ar = rectOf(a);
1283
+ const lr = rectOf(dialogLayer);
1284
+ const k = dialogLayer.offsetWidth ? lr.width / dialogLayer.offsetWidth : 1;
1285
+ const next = { left: ((place.end ? ar.right : ar.left) - lr.left) / (k || 1), top: ((place.top ? ar.top - 6 : ar.bottom + 6) - lr.top) / (k || 1) };
1286
+ if (!pos || Math.abs(pos.left - next.left) > 0.5 || Math.abs(pos.top - next.top) > 0.5)
1287
+ setPos(next);
1288
+ });
1289
+ const shift = `${place.end ? 'translateX(-100%)' : ''} ${place.top ? 'translateY(-100%)' : ''}`.trim() || undefined;
1290
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { minWidth: 180, padding: 4, background: SURFACE, boxShadow: 'var(--jp-shadow)', display: 'flex', flexDirection: 'column' } });
1291
+ const panel = (_jsxs(motion.div, { ...root, role: "menu", initial: animate ? { opacity: 0, y: place.top ? 4 : -4 } : false, animate: { opacity: 1, y: 0 }, transition: { duration: 0.16 }, children: [_jsx(Rough, { seedKey: node.id, strokeWidth: 1.4 }), children] }));
1292
+ if (dialogLayer && pos)
1293
+ return createPortal(_jsx("div", { style: { position: 'absolute', left: pos.left, top: pos.top, transform: shift, zIndex: 5 }, children: panel }), dialogLayer);
1294
+ // 还没测量(首帧、服务端渲染的截图):先在 Menu 旁边原位浮出
1295
+ return _jsx("div", { style: { position: 'absolute', zIndex: 30, ...(place.top ? { bottom: '100%', marginBottom: 6 } : { top: '100%', marginTop: 6 }), ...(place.end ? { right: 0 } : { left: 0 }) }, children: panel });
1296
+ }
1297
+ function MenuItem({ node, p, disabled, children }) {
1298
+ const c = color(p.color);
1299
+ const root = useRoot(node, p, { disabled, className: '[.jstage[data-stroke=clean]_&]:rounded', style: { display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '8px 10px', fontSize: 14, textAlign: 'left', background: 'transparent', border: 'none', color: disabled ? SOFT : (c ?? INK), cursor: 'inherit' } });
1300
+ return (_jsxs("button", { ...root, type: "button", role: "menuitem", "data-jcontrol": node.id, children: [p.leftSection !== undefined ? _jsx("span", { "aria-hidden": true, style: { width: 18, textAlign: 'center' }, children: str(p.leftSection) }) : null, _jsx("span", { className: "jtrunc", style: { flex: 1 }, "data-jtext": "", children: children }), p.rightSection !== undefined ? _jsx("span", { style: { fontSize: 12, color: DIMMED }, children: str(p.rightSection) }) : null] }));
1301
+ }
1302
+ function MenuLabel({ node, p, children }) {
1303
+ const root = useRoot(node, p, { style: { padding: '6px 10px 4px', fontSize: 12, fontWeight: 600, color: DIMMED } });
1304
+ return _jsx("div", { ...root, children: children });
1305
+ }
1306
+ function MenuDivider({ node, p }) {
1307
+ const root = useRoot(node, p, { style: { height: 1, background: LINE, margin: '4px 6px' } });
1308
+ return _jsx("div", { ...root, role: "separator" });
1309
+ }
1310
+ /** TabBar:手机底部标签栏;当前值来自 state.inputs(select 写入)或 value / defaultValue。 */
1311
+ function TabBar({ node, p, disabled }) {
1312
+ const { state, preview } = useCtx();
1313
+ const v = useInputValue(node, p);
1314
+ const items = arr(p.data).map((d) => (d && typeof d === 'object' ? d : { value: d, label: d }));
1315
+ const choices = choicesOf(node, state) ?? [];
1316
+ const pv = preview && preview.nodeId === node.id ? preview.text : null;
1317
+ const cur = pv !== null ? (choices.find((c) => c.value === pv || c.label === pv)?.value ?? null) : v !== undefined && v !== null && v !== '' ? str(v) : (choices[0]?.value ?? null);
1318
+ const c = color(p.color) ?? INK;
1319
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', alignItems: 'stretch', width: '100%', marginTop: 'auto', position: 'sticky', bottom: 0, zIndex: 3, background: SURFACE, borderTop: `1.5px solid ${LINE_STRONG}`, padding: '6px 4px 8px' } });
1320
+ return (_jsx("nav", { ...root, role: "tablist", "aria-label": p['aria-label'] !== undefined ? str(p['aria-label']) : undefined, "data-jcontrol": node.id, children: items.map((it, i) => {
1321
+ const value = str(it.value);
1322
+ const on = value === cur;
1323
+ const badge = it.badge !== undefined && it.badge !== null && it.badge !== '' && it.badge !== 0 ? str(it.badge) : null;
1324
+ return (_jsxs("span", { role: "tab", "aria-selected": on, "data-jchoice": value, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, fontSize: 11, color: on ? c : DIMMED, fontWeight: on ? 700 : 500, position: 'relative' }, children: [_jsx("span", { "aria-hidden": true, style: { fontSize: 20, lineHeight: '24px', filter: on ? undefined : 'grayscale(1)', opacity: on ? 1 : 0.7 }, children: it.icon !== undefined ? str(it.icon) : '•' }), _jsx("span", { className: "jtrunc", style: { maxWidth: '100%' }, children: str(it.label ?? it.value) }), badge ? _jsx("span", { style: { position: 'absolute', top: -2, left: '55%', minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: DANGER, color: '#fff', fontSize: 10, lineHeight: '16px', textAlign: 'center' }, children: badge }) : null] }, `${value}:${i}`));
1325
+ }) }));
1326
+ }
1327
+ // ============ 注册表:目录中每个类型都必须有实现(Record<ComponentType, …> 由类型检查保证) ============
1328
+ // ============ 白板(D96):多屏画面摆在一块白板上,箭头连成流程,便签写说明 ============
1329
+ /** 白板:点阵纸,默认铺满父容器(放进 content:Canvas 就能用镜头在各屏之间移动);子元素用 pos="absolute" top / left 摆放。 */
1330
+ function Board({ node, p, children }) {
1331
+ const dots = p.dots !== false;
1332
+ const root = useRoot(node, p, { style: { position: 'relative', width: '100%', height: '100%', background: dots ? `radial-gradient(circle, ${LINE_STRONG} 1.3px, transparent 1.5px) 0 0 / 28px 28px, ${SURFACE}` : SURFACE } });
1333
+ return _jsx("div", { ...root, "data-jboard": "", children: children });
1334
+ }
1335
+ /** 浏览器窗口外框:一屏画面;label 是框上方的手写标题(“1. 登录”)。 */
1336
+ function BrowserWindow({ node, p, children }) {
1337
+ const mode = useStroke();
1338
+ const root = useRoot(node, p, { className: cn('jbox', mode === 'clean' && 'rounded-lg'), style: { display: 'flex', flexDirection: 'column', background: SURFACE, width: 520, height: 400, flexShrink: 0 } });
1339
+ return (_jsxs("div", { ...root, children: [_jsx(Rough, { seedKey: node.id, strokeWidth: 1.6 }), p.label !== undefined ? _jsx("div", { "aria-hidden": true, style: { position: 'absolute', left: 2, bottom: '100%', marginBottom: 10, fontSize: 22, color: DIMMED, whiteSpace: 'nowrap' }, children: str(p.label) }) : null, _jsxs("div", { className: "flex shrink-0 items-center gap-2 px-3", style: { height: 38, borderBottom: mode === 'clean' ? `1px solid ${LINE}` : undefined, position: 'relative' }, children: [[0, 1, 2].map(i => _jsx("span", { className: "jbox", "data-jround": "", style: { width: 11, height: 11, borderRadius: 999 }, children: _jsx(Rough, { seedKey: `${node.id}:dot${i}`, shape: "ellipse", strokeWidth: 1.1, inset: 0, stroke: SOFT }) }, i)), _jsxs("span", { className: "jbox jtrunc", style: { flex: 1, height: 22, marginLeft: 6, fontSize: 13, color: DIMMED, display: 'flex', alignItems: 'center', padding: '0 10px', borderRadius: 6, background: SUBTLE }, children: [_jsx(Rough, { seedKey: `${node.id}:url`, strokeWidth: 1, roughness: 0.8, inset: 1, stroke: SOFT }), _jsx("span", { children: str(p.url) })] }), mode === 'rough' ? _jsx("span", { className: "jbox", style: { position: 'absolute', left: 4, right: 4, bottom: -2, height: 4 }, children: _jsx(Rough, { seedKey: `${node.id}:bar`, shape: "line", strokeWidth: 1.1, inset: 0, stroke: SOFT }) }) : null] }), _jsx("div", { style: { flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden' }, children: children })] }));
1340
+ }
1341
+ /** 便签:一直留在白板上的说明(callout 只在一步里出现)。 */
1342
+ function Note({ node, p, children }) {
1343
+ const c = color(p.color ?? 'yellow') ?? WARN;
1344
+ const deg = typeof p.rotate === 'number' && Number.isFinite(p.rotate) ? p.rotate : -2;
1345
+ const root = useRoot(node, p, { className: 'jbox', style: { width: 240, padding: '14px 16px', fontSize: 17, lineHeight: 1.4, color: INK, whiteSpace: 'pre-line', background: tint(c, 26), boxShadow: '0 6px 14px rgba(0,0,0,.08)', rotate: `${deg}deg` } });
1346
+ return (_jsxs("div", { ...root, children: [_jsx(Rough, { seedKey: node.id, stroke: tint(c, 70), strokeWidth: 1, roughness: 0.7, inset: 0 }), _jsx("div", { "data-jtext": "", children: children })] }));
1347
+ }
1348
+ /** 元素在白板里的布局位置:offset 链,不受镜头缩放和 FLIP transform 影响。 */
1349
+ function offsetInBoard(el, root) {
1350
+ let x = 0, y = 0;
1351
+ let n = el;
1352
+ while (n && n !== root) {
1353
+ x += n.offsetLeft;
1354
+ y += n.offsetTop;
1355
+ n = n.offsetParent;
1356
+ }
1357
+ return n === root ? { x, y, w: el.offsetWidth, h: el.offsetHeight } : null;
1358
+ }
1359
+ const cssEscape = (id) => (typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"'));
1360
+ /** 量出白板上若干元素的位置;目标出现、消失、改尺寸、字体加载完成都会重新量;没渲染的目标给 null。 */
1361
+ function useBoardRects(ids) {
1362
+ const ref = useRef(null);
1363
+ const [rects, setRects] = useState(null);
1364
+ const key = ids.join('\n');
1365
+ useLayoutEffect(() => {
1366
+ const svg = ref.current;
1367
+ const board = svg?.closest('[data-jboard]');
1368
+ if (!svg || !board)
1369
+ return;
1370
+ let last = '';
1371
+ const find = (id) => board.querySelector(`[data-jid="${cssEscape(id)}"]`);
1372
+ const measure = () => {
1373
+ const next = ids.map(id => { const el = find(id); return el && el.offsetParent ? offsetInBoard(el, board) : null; });
1374
+ const sig = JSON.stringify(next);
1375
+ if (sig !== last) {
1376
+ last = sig;
1377
+ setRects(next);
1378
+ }
1379
+ };
1380
+ measure();
1381
+ const ro = new ResizeObserver(measure);
1382
+ ro.observe(board);
1383
+ for (const id of ids) {
1384
+ const el = find(id);
1385
+ if (el)
1386
+ ro.observe(el);
1387
+ }
1388
+ const mo = new MutationObserver(measure);
1389
+ mo.observe(board, { childList: true, subtree: true });
1390
+ let alive = true;
1391
+ document.fonts?.ready.then(() => { if (alive)
1392
+ measure(); });
1393
+ return () => { alive = false; ro.disconnect(); mo.disconnect(); };
1394
+ }, [key]);
1395
+ return { ref, rects };
1396
+ }
1397
+ /** 从 r 的中心朝 (tx, ty) 出发,落在外扩 pad 的边框上的点。 */
1398
+ function edgePoint(r, tx, ty, pad) {
1399
+ const cx = r.x + r.w / 2, cy = r.y + r.h / 2;
1400
+ const dx = tx - cx, dy = ty - cy;
1401
+ if (!dx && !dy)
1402
+ return { x: cx, y: cy };
1403
+ const t = Math.min(dx ? (r.w / 2 + pad) / Math.abs(dx) : Infinity, dy ? (r.h / 2 + pad) / Math.abs(dy) : Infinity);
1404
+ return { x: cx + dx * t, y: cy + dy * t };
1405
+ }
1406
+ /** 两个元素之间一直保留的箭头(引擎的 mark 只在一步里出现)。手绘描边用 Rough.js,干净线框退回普通 SVG。 */
1407
+ function Arrow({ node, p }) {
1408
+ const mode = useStroke();
1409
+ const { animate } = useCtx();
1410
+ const attrs = useRootAttrs(node);
1411
+ const { ref, rects } = useBoardRects([str(p.from), str(p.to)]);
1412
+ const stroke = color(p.color) ?? INK;
1413
+ const a = rects?.[0], b = rects?.[1];
1414
+ let d = '', at = null;
1415
+ if (a && b) {
1416
+ const s = edgePoint(a, b.x + b.w / 2, b.y + b.h / 2, 10), e = edgePoint(b, a.x + a.w / 2, a.y + a.h / 2, 14);
1417
+ const len = Math.hypot(e.x - s.x, e.y - s.y) || 1;
1418
+ const bend = Math.max(-1, Math.min(1, typeof p.bend === 'number' && Number.isFinite(p.bend) ? p.bend : 0.25));
1419
+ const c = { x: (s.x + e.x) / 2 - ((e.y - s.y) / len) * bend * len * 0.3, y: (s.y + e.y) / 2 + ((e.x - s.x) / len) * bend * len * 0.3 };
1420
+ const ang = Math.atan2(e.y - c.y, e.x - c.x), head = 16, spread = 0.45;
1421
+ const f = (n) => n.toFixed(1);
1422
+ d = `M${f(s.x)},${f(s.y)} Q${f(c.x)},${f(c.y)} ${f(e.x)},${f(e.y)} M${f(e.x - head * Math.cos(ang - spread))},${f(e.y - head * Math.sin(ang - spread))} L${f(e.x)},${f(e.y)} L${f(e.x - head * Math.cos(ang + spread))},${f(e.y - head * Math.sin(ang + spread))}`;
1423
+ at = { x: 0.25 * s.x + 0.5 * c.x + 0.25 * e.x, y: 0.25 * s.y + 0.5 * c.y + 0.25 * e.y };
1424
+ }
1425
+ const dashed = p.dashed === true;
1426
+ return (_jsxs(_Fragment, { children: [_jsx("svg", { ref: ref, ...attrs, "aria-label": nodeLabel(node), style: { position: 'absolute', left: 0, top: 0, width: 1, height: 1, overflow: 'visible', pointerEvents: 'none', zIndex: 5 }, children: d ? (mode === 'clean'
1427
+ ? _jsx("path", { d: d, fill: "none", stroke: stroke, strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", strokeDasharray: dashed ? '7 6' : undefined })
1428
+ : _jsx(RoughPath, { seedKey: `${node.id}:${d.length}`, d: d, stroke: stroke, strokeWidth: 1.8, dashed: dashed, roughness: 1, draw: animate })) : null }), at && p.label !== undefined ? _jsx("div", { "aria-hidden": true, "data-jtext": "", style: { position: 'absolute', left: at.x, top: at.y, transform: 'translate(-50%, -50%)', background: SURFACE, padding: '1px 8px', fontSize: 17, color: p.color !== undefined ? stroke : DIMMED, whiteSpace: 'nowrap', pointerEvents: 'none', zIndex: 6 }, children: str(p.label) }) : null] }));
1429
+ }
1430
+ const IMPL = {
1431
+ AppShell,
1432
+ 'AppShell.Header': ShellHeader,
1433
+ 'AppShell.Navbar': ShellSide,
1434
+ 'AppShell.Main': ShellMain,
1435
+ 'AppShell.Aside': ShellSide,
1436
+ 'AppShell.Footer': ShellFooter,
1437
+ Container,
1438
+ Stack,
1439
+ Group,
1440
+ Flex,
1441
+ Grid,
1442
+ 'Grid.Col': GridCol,
1443
+ SimpleGrid,
1444
+ Center,
1445
+ Box,
1446
+ Space,
1447
+ Paper,
1448
+ Card,
1449
+ 'Card.Section': CardSection,
1450
+ Divider,
1451
+ ScrollArea,
1452
+ Title,
1453
+ Text,
1454
+ Code,
1455
+ Kbd,
1456
+ Blockquote,
1457
+ List,
1458
+ 'List.Item': ListItem,
1459
+ Badge,
1460
+ Avatar,
1461
+ Image: ImageBox,
1462
+ ThemeIcon,
1463
+ Indicator,
1464
+ Timeline,
1465
+ 'Timeline.Item': TimelineItem,
1466
+ Table,
1467
+ 'Table.Thead': tablePart('thead'),
1468
+ 'Table.Tbody': tablePart('tbody'),
1469
+ 'Table.Tr': tablePart('tr'),
1470
+ 'Table.Th': tablePart('th'),
1471
+ 'Table.Td': tablePart('td'),
1472
+ DataTable,
1473
+ Accordion,
1474
+ 'Accordion.Item': AccordionItem,
1475
+ 'Accordion.Panel': AccordionPanel,
1476
+ Radio,
1477
+ 'Tabs.List': TabsList,
1478
+ 'Tabs.Panel': TabsPanel,
1479
+ Breadcrumbs,
1480
+ Stepper,
1481
+ 'Stepper.Step': StepperStep,
1482
+ Alert,
1483
+ Loader,
1484
+ Progress,
1485
+ RingProgress,
1486
+ Skeleton,
1487
+ Notification,
1488
+ Modal: Overlay,
1489
+ Drawer: Overlay,
1490
+ ChatMessage,
1491
+ ToolCall,
1492
+ Thinking,
1493
+ Email,
1494
+ DataCard,
1495
+ StateFlow,
1496
+ Artifact,
1497
+ Terminal,
1498
+ 'Terminal.Command': TerminalCommand,
1499
+ 'Terminal.Output': TerminalOutput,
1500
+ Phone,
1501
+ ...Object.fromEntries(HTML_TAGS.map((t) => [t, htmlImpl(t)])),
1502
+ Placeholder,
1503
+ // 产品界面(D68)
1504
+ 'Avatar.Group': AvatarGroup,
1505
+ 'Radio.Card': RadioCard,
1506
+ EmptyState,
1507
+ ListRow,
1508
+ Stat,
1509
+ KeyValue,
1510
+ BarChart,
1511
+ LineChart,
1512
+ Menu,
1513
+ 'Menu.Target': MenuTarget,
1514
+ 'Menu.Dropdown': MenuDropdown,
1515
+ 'Menu.Item': MenuItem,
1516
+ 'Menu.Label': MenuLabel,
1517
+ 'Menu.Divider': MenuDivider,
1518
+ TabBar,
1519
+ Button,
1520
+ ActionIcon,
1521
+ CloseButton,
1522
+ Anchor,
1523
+ a: Anchor,
1524
+ NavLink,
1525
+ Burger,
1526
+ TextInput,
1527
+ PasswordInput: TextInput,
1528
+ NumberInput: TextInput,
1529
+ Textarea,
1530
+ Select,
1531
+ MultiSelect: Select,
1532
+ Checkbox,
1533
+ 'Radio.Group': RadioGroup,
1534
+ Switch,
1535
+ SegmentedControl,
1536
+ FileInput: TextInput,
1537
+ Tabs,
1538
+ 'Tabs.Tab': TabsTab,
1539
+ 'Accordion.Control': AccordionControl,
1540
+ Pagination,
1541
+ PromptInput,
1542
+ 'Artifact.Ref': ArtifactRef,
1543
+ 'Terminal.Input': TerminalInput,
1544
+ Board,
1545
+ BrowserWindow,
1546
+ Note,
1547
+ Arrow,
1548
+ };
1549
+ export const IMPLEMENTED_TYPES = Object.keys(IMPL).map(name => `ui:${name}`);
1550
+ export default defineRuntime({
1551
+ id: 'ui',
1552
+ components: Object.fromEntries(Object.entries(IMPL).map(([name, Impl]) => [name, { react: ({ ctx }) => _jsx(Impl, { node: ctx.model, p: ctx.node.props, disabled: ctx.node.disabled, children: ctx.renderChildren() }) }])),
1553
+ viewports: { mobile: { insets: { x: 4 + PHONE_BEZEL + PHONE_SAFE.x, y: PHONE_BEZEL + PHONE_SAFE.top }, size: phoneFrameSize, react: PhoneFrame } },
1554
+ });