@joplo-app/plugin-ui 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1447 @@
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 } 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
+ export const embeddedImage = (src) => (typeof src === 'string' && src.startsWith('data:image/') ? src : null);
364
+ function ImageBox({ node, p }) {
365
+ const { t: i18n } = useI18n();
366
+ const r = useR(p);
367
+ const src = embeddedImage(p.src);
368
+ if (src) {
369
+ const h = dim(r('h'));
370
+ const fit = p.fit === 'contain' ? 'contain' : 'cover';
371
+ const root = useRoot(node, p, { style: { display: 'block', position: 'relative', overflow: 'hidden', width: dim(r('w')) ?? '100%', height: h ?? 'auto', borderRadius: radius(p.radius) } });
372
+ 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') } }) }));
373
+ }
374
+ 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) } });
375
+ 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)] })] }));
376
+ }
377
+ function ThemeIcon({ node, p, children }) {
378
+ const size = typeof p.size === 'number' ? p.size : (AVATAR[str(p.size, 'md')] ?? 28) * 0.8;
379
+ const c = color(p.color) ?? INK;
380
+ const filled = str(p.variant, 'filled') === 'filled';
381
+ 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 } });
382
+ return (_jsx("span", { ...root, children: _jsx("span", { children: children }) }));
383
+ }
384
+ function Indicator({ node, p, disabled, children }) {
385
+ const c = color(p.color) ?? DANGER;
386
+ const label = p.label !== undefined ? str(p.label) : '';
387
+ const root = useRoot(node, p, { style: { position: 'relative', display: p.inline ? 'inline-block' : 'block', width: 'fit-content' } });
388
+ 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 }))] }));
389
+ }
390
+ function Timeline({ node, p, children }) {
391
+ const active = Number(resolveValue(p.active, useCtx().state) ?? -1);
392
+ const ids = (node.children ?? []).filter((c) => c.type === 'ui:Timeline.Item').map((c) => c.id);
393
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column' } });
394
+ return (_jsx(ItemProgressContext.Provider, { value: { ids, active: Number.isFinite(active) ? active : -1, content: null }, children: _jsx("div", { ...root, children: children }) }));
395
+ }
396
+ function TimelineItem({ node, p, children }) {
397
+ const seq = useContext(ItemProgressContext);
398
+ const i = seq ? seq.ids.indexOf(node.id) : 0;
399
+ const done = !!seq && i <= seq.active;
400
+ const last = !!seq && i === seq.ids.length - 1;
401
+ const c = color(p.color) ?? INK;
402
+ const root = useRoot(node, p, { style: { position: 'relative', paddingLeft: 30, paddingBottom: last ? 0 : 18 } });
403
+ 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 })] }));
404
+ }
405
+ // ============ Table / DataTable ============
406
+ const cellPad = '8px 12px';
407
+ function TableShell({ node, p, children }) {
408
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { width: '100%', background: SURFACE, overflowX: 'auto' } });
409
+ return (_jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: INK2, strokeWidth: 1.1 }), children] }));
410
+ }
411
+ function Table({ node, p, children }) {
412
+ const data = p.data && typeof p.data === 'object' && !Array.isArray(p.data) ? p.data : null;
413
+ const striped = p.striped === true;
414
+ 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) }) }));
415
+ }
416
+ const tablePart = (Tag) => function TablePart({ node, p, children }) {
417
+ const root = useRoot(node, p);
418
+ return _jsx(Tag, { ...root, children: children });
419
+ };
420
+ function getPath(o, path) {
421
+ let cur = o;
422
+ for (const k of path.split('.')) {
423
+ if (cur === null || typeof cur !== 'object')
424
+ return undefined;
425
+ cur = cur[k];
426
+ }
427
+ return cur;
428
+ }
429
+ const humanize = (s) => s.replace(/[_.-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
430
+ /** 选中的行:selectedRecords 里的 id(或带 idAccessor 的记录)。 */
431
+ export function selectedIds(selected, idAccessor) {
432
+ return new Set(arr(selected).map((x) => (x && typeof x === 'object' ? str(getPath(x, idAccessor)) : str(x))).filter(Boolean));
433
+ }
434
+ /**
435
+ * DataTable(mantine-datatable):records(可绑定)+ columns [{ accessor, title, textAlign, width, sortable, badge, avatar }] + noRecordsText。
436
+ * D68:sortStatus(表头箭头,只是显示,排好序的 records 由步骤 set 进去)、selectedRecords + idAccessor(勾选列、选中行底色)、
437
+ * fetching(加载遮罩)。单元格仍然只是文字,badge / avatar 让最常见的两种单元格不必改写成 Table.*。
438
+ */
439
+ function DataTable({ node, p }) {
440
+ const { t: i18n } = useI18n();
441
+ const cols = arr(p.columns)
442
+ .filter((c) => !!c && typeof c === 'object' && c.hidden !== true)
443
+ .map((c, i) => {
444
+ const accessor = str(c.accessor ?? c.accessorKey ?? `col${i}`);
445
+ const badge = c.badge === true ? true : c.badge && typeof c.badge === 'object' && !Array.isArray(c.badge) ? c.badge : null;
446
+ 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 };
447
+ });
448
+ const rows = arr(p.records ?? p.data);
449
+ const sort = p.sortStatus && typeof p.sortStatus === 'object' ? p.sortStatus : null;
450
+ const sortCol = sort ? str(sort.columnAccessor) : null;
451
+ const sortDir = sort && str(sort.direction) === 'desc' ? 'desc' : 'asc';
452
+ const idAccessor = str(p.idAccessor, 'id');
453
+ const selectable = p.selectedRecords !== undefined;
454
+ const selected = selectedIds(p.selectedRecords, idAccessor);
455
+ const allOn = selectable && rows.length > 0 && rows.every((r) => selected.has(r && typeof r === 'object' ? str(getPath(r, idAccessor)) : str(r)));
456
+ const someOn = selectable && !allOn && selected.size > 0;
457
+ const fetching = p.fetching === true;
458
+ // 勾选框不用 jbox(手绘描边会把边框交给 Rough),未勾选时也要看得见
459
+ 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 ? '−' : '' }));
460
+ const cell = (c, r) => {
461
+ const v = r && typeof r === 'object' ? str(getPath(r, c.accessor)) : str(r);
462
+ if (c.badge && v) {
463
+ const tone = c.badge === true ? undefined : color(c.badge[v]);
464
+ const base = tone ?? INK2;
465
+ 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 });
466
+ }
467
+ if (c.avatar && v)
468
+ 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 })] }));
469
+ return v;
470
+ };
471
+ 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) => {
472
+ const sorted = sortCol === c.accessor;
473
+ 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));
474
+ })] }) }), _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) => {
475
+ const on = selectable && selected.has(r && typeof r === 'object' ? str(getPath(r, idAccessor)) : str(r));
476
+ 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));
477
+ })) })] }), 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] }) }));
478
+ }
479
+ // ============ Accordion ============
480
+ function Accordion({ node, p, children }) {
481
+ const v = useInputValue(node, p);
482
+ const open = new Set(Array.isArray(v) ? v.map(String) : v !== undefined && v !== null && v !== '' ? [str(v)] : []);
483
+ const root = useRoot(node, p, { className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md', style: { display: 'flex', flexDirection: 'column', background: SURFACE } });
484
+ return (_jsx(AccordionContext.Provider, { value: { owner: node.id, open }, children: _jsxs("div", { ...root, "data-soft": "", children: [_jsx(Rough, { seedKey: node.id, stroke: SOFT }), children] }) }));
485
+ }
486
+ function AccordionItem({ node, p, children }) {
487
+ const acc = useContext(AccordionContext);
488
+ const value = str(p.value);
489
+ const root = useRoot(node, p, { className: 'jacc-item', style: { borderBottom: `1px solid ${LINE}` } });
490
+ return (_jsx(AccordionItemContext.Provider, { value: { value, open: !!acc?.open.has(value) }, children: _jsx("div", { ...root, children: children }) }));
491
+ }
492
+ function AccordionControl({ node, p, disabled, children }) {
493
+ const item = useContext(AccordionItemContext);
494
+ const owner = useContext(AccordionContext);
495
+ const ctx = useCtx();
496
+ 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 } });
497
+ return (_jsxs("button", { ...root, type: "button", "aria-expanded": !!item?.open, onClick: ctx.interactive ? e => { e.stopPropagation(); if (!disabled && owner && item)
498
+ 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" })] }));
499
+ }
500
+ function AccordionPanel({ node, p, children }) {
501
+ const item = useContext(AccordionItemContext);
502
+ const root = useRoot(node, p, { style: { padding: '0 14px 12px', whiteSpace: 'pre-line', fontSize: 14, color: INK2 } });
503
+ if (item && !item.open)
504
+ return null;
505
+ return _jsx("div", { ...root, children: children });
506
+ }
507
+ // ============ Alert / Notification / Loader / Progress ============
508
+ function Alert({ node, p, children }) {
509
+ const mode = useStroke();
510
+ const c = color(p.color) ?? INFO;
511
+ const variant = str(p.variant, 'light');
512
+ const filled = variant === 'filled';
513
+ const danger = p.color === 'red';
514
+ const root = useRoot(node, p, {
515
+ className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md',
516
+ 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 } : {}) },
517
+ });
518
+ 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] }));
519
+ }
520
+ /** Notification 的浮动位置(D68):没写 position 时内联;写了时浮在当前页面(弹窗层)的那个角或那条边上。 */
521
+ export const NOTIFICATION_POSITIONS = ['top', 'top-left', 'top-right', 'bottom', 'bottom-left', 'bottom-right'];
522
+ export function notificationPlacement(position) {
523
+ if (typeof position !== 'string' || !NOTIFICATION_POSITIONS.includes(position))
524
+ return null;
525
+ const [v, hpos] = position.split('-');
526
+ return { position: 'absolute', [v]: 16, ...(hpos ? { [hpos]: 16 } : { left: '50%', transform: 'translateX(-50%)' }), width: 'min(380px, calc(100% - 32px))', zIndex: 6 };
527
+ }
528
+ function Notification({ node, p, children }) {
529
+ const { dialogLayer, animate } = useCtx();
530
+ const c = color(p.color) ?? INFO;
531
+ const place = notificationPlacement(p.position);
532
+ 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' } });
533
+ 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" }))] }));
534
+ if (!place)
535
+ return body;
536
+ 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 }) }));
537
+ // 浮到弹窗层(跟着镜头),没有弹窗层(服务端渲染)时相对最近的定位祖先
538
+ return dialogLayer ? createPortal(float, dialogLayer) : float;
539
+ }
540
+ function Spinner({ size = 18, c = INK, kind = 'oval' }) {
541
+ const { animate } = useCtx();
542
+ if (kind === 'dots')
543
+ 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))) }));
544
+ 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 }));
545
+ }
546
+ const LOADER = { xs: 14, sm: 18, md: 24, lg: 32, xl: 40 };
547
+ function Loader({ node, p, children }) {
548
+ const size = typeof p.size === 'number' ? p.size : (LOADER[str(p.size, 'md')] ?? 24);
549
+ const root = useRoot(node, p, { style: { display: 'inline-flex', alignItems: 'center', gap: 8, color: INK2 } });
550
+ 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] }));
551
+ }
552
+ const PROGRESS_H = { xs: 3, sm: 5, md: 8, lg: 12, xl: 16 };
553
+ function Progress({ node, p }) {
554
+ const v = Math.max(0, Math.min(100, Number(p.value ?? 0) || 0));
555
+ const h = typeof p.size === 'number' ? p.size : (PROGRESS_H[str(p.size, 'md')] ?? 8);
556
+ const c = color(p.color) ?? INK;
557
+ const root = useRoot(node, p, { className: 'jbox', style: { width: '100%', height: h + 4, background: SUBTLE, borderRadius: 999, overflow: 'hidden' } });
558
+ 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 } })] }));
559
+ }
560
+ function RingProgress({ node, p }) {
561
+ const size = typeof p.size === 'number' ? p.size : 120;
562
+ const th = typeof p.thickness === 'number' ? p.thickness : size / 10;
563
+ const rr = (size - th) / 2;
564
+ const L = 2 * Math.PI * rr;
565
+ let acc = 0;
566
+ const sections = arr(p.sections).filter((s) => !!s && typeof s === 'object');
567
+ const root = useRoot(node, p, { style: { position: 'relative', width: size, height: size, flexShrink: 0 } });
568
+ 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) => {
569
+ const v = Math.max(0, Math.min(100, Number(s.value) || 0));
570
+ 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);
571
+ acc += v;
572
+ return el;
573
+ })] }), 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] }));
574
+ }
575
+ function Skeleton({ node, p, children }) {
576
+ const { animate } = useCtx();
577
+ const r = useR(p);
578
+ const h = dim(r('height') ?? r('h')) ?? (p.circle ? '40px' : '16px');
579
+ 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 } });
580
+ if (p.visible === false)
581
+ return _jsx(_Fragment, { children: children });
582
+ // D68:加载中轻轻呼吸(截图、导出与 animate=false 时静止)
583
+ const pulse = animate && p.animate !== false;
584
+ 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 });
585
+ }
586
+ // ============ Modal / Drawer ============
587
+ const OVERLAY_SIZE = { xs: 320, sm: 380, md: 440, lg: 620, xl: 780 };
588
+ function sizeOf(v, d) {
589
+ if (typeof v === 'number')
590
+ return `${v}px`;
591
+ if (typeof v === 'string' && v)
592
+ return v in OVERLAY_SIZE ? `${OVERLAY_SIZE[v]}px` : v;
593
+ return `${d}px`;
594
+ }
595
+ /** Modal / Drawer:渲染到舞台内的弹窗层(不 portal 到 body),带遮罩。门控(opened)已由 isSelfVisible 判断。 */
596
+ function Overlay({ node, p, children }) {
597
+ const { t: i18n } = useI18n();
598
+ const { dialogLayer, animate } = useCtx();
599
+ const drawer = node.type === 'ui:Drawer';
600
+ const position = str(p.position, 'left');
601
+ const full = p.fullScreen === true;
602
+ const horizontal = drawer && (position === 'left' || position === 'right');
603
+ const title = p.title !== undefined ? str(p.title) : '';
604
+ const panelStyle = drawer
605
+ ? horizontal
606
+ ? { height: '100%', width: sizeOf(p.size, 440), maxWidth: '90%' }
607
+ : { width: '100%', height: sizeOf(p.size, 320), maxHeight: '90%' }
608
+ : full
609
+ ? { width: '100%', height: '100%' }
610
+ : { width: sizeOf(p.size, 440), maxWidth: '92%', maxHeight: '90%', ...(p.centered ? {} : { marginTop: '6%' }) };
611
+ const root = useRoot(node, p, {
612
+ 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'),
613
+ style: { ...panelStyle, overflow: 'auto' },
614
+ });
615
+ if (!dialogLayer)
616
+ return null;
617
+ const align = drawer
618
+ ? { alignItems: position === 'bottom' ? 'flex-end' : 'flex-start', justifyContent: position === 'right' ? 'flex-end' : 'flex-start', flexDirection: horizontal ? 'row' : 'column' }
619
+ : { alignItems: p.centered || full ? 'center' : 'flex-start', justifyContent: 'center' };
620
+ 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 };
621
+ 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);
622
+ }
623
+ // ============ 按钮 ============
624
+ 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 };
625
+ function buttonTone(variant, c) {
626
+ const base = c ?? INK;
627
+ switch (variant) {
628
+ case 'light':
629
+ return { stroke: light(base), fill: light(base), text: base };
630
+ case 'outline':
631
+ return { stroke: base, text: base };
632
+ case 'subtle':
633
+ case 'transparent':
634
+ return { stroke: 'transparent', text: base };
635
+ case 'white':
636
+ return { stroke: LINE, fill: SURFACE, text: base };
637
+ case 'default':
638
+ return { stroke: LINE_STRONG, fill: SURFACE, text: INK };
639
+ default:
640
+ return { stroke: base, fill: base, text: onFill(base) };
641
+ }
642
+ }
643
+ function Button({ node, p, disabled, children }) {
644
+ const mode = useStroke();
645
+ const variant = str(p.variant, 'filled');
646
+ const t = buttonTone(variant, color(p.color));
647
+ const h = BTN_H[str(p.size, 'md')] ?? 40;
648
+ const loading = p.loading === true;
649
+ const stroke = disabled ? SOFT : t.stroke;
650
+ const fill = disabled ? (t.fill && t.fill !== SURFACE ? LINE_STRONG : undefined) : t.fill;
651
+ const text = disabled ? (t.fill && t.fill !== SURFACE ? INK2 : DIMMED) : t.text;
652
+ const root = useRoot(node, p, {
653
+ disabled,
654
+ className: cn('jbox', mode === 'clean' && 'rounded-md', disabled && 'cursor-not-allowed'),
655
+ style: {
656
+ display: 'inline-flex',
657
+ alignItems: 'center',
658
+ justifyContent: 'center',
659
+ gap: 6,
660
+ height: h,
661
+ padding: `0 ${Math.round(h * 0.45)}px`,
662
+ fontSize: h <= 30 ? 13 : 15,
663
+ fontWeight: 600,
664
+ color: text,
665
+ maxWidth: '100%',
666
+ width: p.fullWidth ? '100%' : undefined,
667
+ flexShrink: 0,
668
+ ...(mode === 'clean' ? { ...boxStyle(stroke, fill), ...(disabled ? { borderStyle: 'dashed' } : {}) } : {}),
669
+ },
670
+ });
671
+ 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] }));
672
+ }
673
+ const ICON_SIZE = { xs: 18, sm: 22, md: 28, lg: 34, xl: 44 };
674
+ function ActionIcon({ node, p, disabled, children }) {
675
+ const mode = useStroke();
676
+ const t = buttonTone(str(p.variant, 'filled'), color(p.color));
677
+ const size = typeof p.size === 'number' ? p.size : (ICON_SIZE[str(p.size, 'md')] ?? 28);
678
+ const round = p.radius === 'xl' || p.radius === 999;
679
+ const fill = disabled ? LINE_STRONG : t.fill;
680
+ const root = useRoot(node, p, {
681
+ disabled,
682
+ className: cn('jbox', disabled && 'cursor-not-allowed'),
683
+ 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) : {}) },
684
+ });
685
+ 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 })] }));
686
+ }
687
+ function CloseButton({ node, p, disabled }) {
688
+ const { t: i18n } = useI18n();
689
+ 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 } });
690
+ return (_jsx("button", { ...root, type: "button", "aria-label": str(p['aria-label'], i18n.stage.close), children: "\u00D7" }));
691
+ }
692
+ function NavLink({ node, p, disabled, children }) {
693
+ const { scene } = useCtx();
694
+ const active = p.active === true || (p.active === undefined && typeof p.href === 'string' && p.href === scene.route);
695
+ const c = color(p.color) ?? INK;
696
+ const opened = p.opened === true || p.defaultOpened === true;
697
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', flexDirection: 'column' } });
698
+ 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] }));
699
+ }
700
+ // ============ 输入 ============
701
+ /** Mantine 输入组件的外壳:label(withAsterisk)/ description / 控件 / error。根节点包含全部,控件带 data-jcontrol。 */
702
+ function Field({ node, p, disabled, control, inline = false }) {
703
+ const root = useRoot(node, p, { disabled, style: inline ? { display: 'flex', alignItems: 'flex-start', gap: 10 } : { display: 'flex', flexDirection: 'column', gap: 4, width: '100%' } });
704
+ const ctx = useCtx();
705
+ const toggle = node.traits?.native?.action === 'toggle';
706
+ if (ctx.interactive && toggle) {
707
+ const activate = () => { if (!disabled)
708
+ ctx.act?.(node.id, { type: 'click' }, { type: 'toggle', target: node.id, value: !Boolean(ctx.state.inputs[node.id] ?? p.checked ?? p.defaultChecked) }); };
709
+ Object.assign(root, { onClick: (e) => { e.stopPropagation(); activate(); }, onKeyDown: (e) => { if (e.key === ' ' || e.key === 'Enter') {
710
+ e.preventDefault();
711
+ e.stopPropagation();
712
+ activate();
713
+ } } });
714
+ }
715
+ const label = p.label !== undefined && p.label !== '' ? str(p.label) : '';
716
+ const desc = p.description !== undefined ? str(p.description) : '';
717
+ const error = typeof p.error === 'string' && p.error ? p.error : '';
718
+ const star = p.withAsterisk === true || p.required === true;
719
+ const head = label ? (_jsxs("span", { style: { fontSize: 14, fontWeight: 500, lineHeight: 1.4 }, children: [label, star ? _jsx("span", { style: { color: DANGER }, children: " *" }) : null] })) : null;
720
+ if (inline)
721
+ 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] }));
722
+ 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] }));
723
+ }
724
+ /** 输入框外观:手绘描边 / 线框边框;error 时描红。 */
725
+ function InputBox({ node, p, disabled, children, minH = 40, open }) {
726
+ const err = typeof p.error === 'string' && p.error !== '';
727
+ 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] }));
728
+ }
729
+ 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)]';
730
+ /** 只读的原生输入框:值来自状态,用户不能直接编辑(输入由步骤驱动)。 */
731
+ function NativeInput({ node, disabled, label, value, placeholder, mask = false }) {
732
+ const ctx = useCtx();
733
+ 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 } });
734
+ }
735
+ function TextInput({ node, p, disabled }) {
736
+ const v = useInputValue(node, p);
737
+ const mask = node.type === 'ui:PasswordInput' || p.type === 'password';
738
+ const prefix = p.prefix !== undefined ? str(p.prefix) : '';
739
+ const suffix = p.suffix !== undefined ? str(p.suffix) : '';
740
+ const has = v !== undefined && v !== null && v !== '';
741
+ const value = has && (prefix || suffix) ? `${prefix}${str(v)}${suffix}` : v;
742
+ 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] }) }));
743
+ }
744
+ function Textarea({ node, p, disabled }) {
745
+ const ctx = useCtx();
746
+ const v = useInputValue(node, p);
747
+ const rows = typeof p.minRows === 'number' ? p.minRows : typeof p.rows === 'number' ? p.rows : 3;
748
+ 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' } }) }) }));
749
+ }
750
+ /** Select / MultiSelect:选项来自 data;显示 inputs[id];选择动画期间展开下拉。 */
751
+ function Select({ node, p, disabled }) {
752
+ const { t: i18n } = useI18n();
753
+ const ctx = useCtx();
754
+ const { state, preview } = ctx;
755
+ const [localOpen, setLocalOpen] = useState(false);
756
+ useLayoutEffect(() => setLocalOpen(false), [ctx.resetKey]);
757
+ const v = useInputValue(node, p);
758
+ const choices = choicesOf(node, state) ?? [];
759
+ const multi = node.type === 'ui:MultiSelect';
760
+ const values = multi ? (Array.isArray(v) ? v.map(String) : []) : v !== undefined && v !== null && v !== '' ? [str(v)] : [];
761
+ const labelOf = (x) => choices.find((c) => c.value === x)?.label ?? x;
762
+ const pv = preview && preview.nodeId === node.id ? preview.text : null;
763
+ const open = pv !== null || !!ctx.interactive && localOpen;
764
+ const previewChoice = pv !== null ? (choices.find((c) => c.value === pv || c.label === pv)?.value ?? null) : null;
765
+ return (_jsx(Field, { node: node, p: p, disabled: disabled, control: _jsxs("div", { className: "relative w-full", onClick: ctx.interactive ? e => { e.stopPropagation(); if (!disabled) {
766
+ setLocalOpen(v => !v);
767
+ ctx.act?.(node.id, { type: 'click' });
768
+ } } : undefined, onKeyDown: ctx.interactive ? e => {
769
+ if (disabled)
770
+ return;
771
+ if (e.key === 'Escape') {
772
+ e.stopPropagation();
773
+ setLocalOpen(false);
774
+ }
775
+ if (e.key === 'Enter' || e.key === ' ') {
776
+ e.preventDefault();
777
+ e.stopPropagation();
778
+ setLocalOpen(v => !v);
779
+ }
780
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
781
+ e.preventDefault();
782
+ e.stopPropagation();
783
+ setLocalOpen(true);
784
+ const index = choices.findIndex(c => c.value === values[0]);
785
+ const next = choices[(index + (e.key === 'ArrowDown' ? 1 : choices.length - 1) + choices.length) % choices.length];
786
+ if (next)
787
+ ctx.act?.(node.id, { type: 'select', value: next.value });
788
+ }
789
+ } : 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) => {
790
+ const on = c.value === previewChoice || values.includes(c.value);
791
+ 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)
792
+ setLocalOpen(false); } : undefined, onKeyDown: ctx.interactive ? e => { if (e.key === 'Enter' || e.key === ' ') {
793
+ e.preventDefault();
794
+ e.stopPropagation();
795
+ ctx.act?.(node.id, { type: 'select', value: c.value });
796
+ if (!multi)
797
+ setLocalOpen(false);
798
+ } } : 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));
799
+ })] })) : null] }) }));
800
+ }
801
+ function SegmentedControl({ node, p, disabled }) {
802
+ const ctx = useCtx();
803
+ const { state, preview } = ctx;
804
+ const v = useInputValue(node, p);
805
+ const choices = choicesOf(node, state) ?? [];
806
+ const pv = preview && preview.nodeId === node.id ? preview.text : null;
807
+ 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);
808
+ const root = useRoot(node, p, {
809
+ disabled,
810
+ className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-md',
811
+ style: { display: p.fullWidth ? 'flex' : 'inline-flex', width: p.fullWidth ? '100%' : 'fit-content', padding: 4, gap: 2, background: SUBTLE },
812
+ });
813
+ 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) => {
814
+ const on = c.value === cur;
815
+ return (_jsxs("span", { role: "radio", tabIndex: ctx.interactive && !disabled ? 0 : undefined, onClick: ctx.interactive ? e => { e.stopPropagation(); if (!disabled)
816
+ 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') {
817
+ e.preventDefault();
818
+ e.stopPropagation();
819
+ if (!disabled)
820
+ ctx.act?.(node.id, { type: 'select', value: c.value });
821
+ } } : 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));
822
+ })] }));
823
+ }
824
+ function Checkbox({ node, p, disabled }) {
825
+ const checked = useInputValue(node, p) === true;
826
+ 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] }) }));
827
+ }
828
+ function Switch({ node, p, disabled }) {
829
+ const on = useInputValue(node, p) === true;
830
+ const ctx = useCtx();
831
+ const scripted = useRef({ value: on, changedLocally: false });
832
+ const changedLocally = !!ctx.playground && on !== scripted.current.value;
833
+ const instant = changedLocally || scripted.current.changedLocally;
834
+ if (!ctx.playground)
835
+ scripted.current = { value: on, changedLocally: false };
836
+ else if (changedLocally)
837
+ scripted.current.changedLocally = true;
838
+ const c = color(p.color) ?? INK;
839
+ 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' } }) }) }));
840
+ }
841
+ function RadioGroup({ node, p, disabled, children }) {
842
+ const v = useInputValue(node, p);
843
+ const label = p.label !== undefined ? str(p.label) : '';
844
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', flexDirection: 'column', gap: 8 } });
845
+ 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 })] }) }));
846
+ }
847
+ function Radio({ node, p, disabled }) {
848
+ const host = useCtx();
849
+ const ctx = useContext(ValueContext);
850
+ const standalone = useInputValue(node, p) === true;
851
+ const on = ctx ? ctx.value !== null && ctx.value === str(p.value) : standalone;
852
+ const root = useRoot(node, p, { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } });
853
+ return (_jsxs("span", { ...root, role: "radio", tabIndex: host.interactive ? 0 : undefined, onClick: host.interactive ? e => { e.stopPropagation(); if (!disabled && !ctx?.disabled)
854
+ 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') {
855
+ e.preventDefault();
856
+ e.stopPropagation();
857
+ e.currentTarget.click();
858
+ } } : 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] }));
859
+ }
860
+ // ============ Tabs / Breadcrumbs / Pagination / Stepper ============
861
+ function Tabs({ node, p, disabled, children }) {
862
+ const { state } = useCtx();
863
+ const v = useInputValue(node, p);
864
+ const first = choicesOf(node, state)?.[0]?.value ?? null;
865
+ const value = v !== undefined && v !== null && v !== '' ? str(v) : first;
866
+ const root = useRoot(node, p, { disabled, style: { display: 'flex', flexDirection: 'column', gap: 12 } });
867
+ return (_jsx(ValueContext.Provider, { value: { owner: node.id, value, disabled }, children: _jsx("div", { ...root, children: children }) }));
868
+ }
869
+ function TabsList({ node, p, children }) {
870
+ 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') } });
871
+ return (_jsx("div", { ...root, role: "tablist", children: children }));
872
+ }
873
+ function TabsTab({ node, p, disabled, children }) {
874
+ const host = useCtx();
875
+ const ctx = useContext(ValueContext);
876
+ const dis = disabled || !!ctx?.disabled;
877
+ const active = ctx?.value !== null && ctx?.value === str(p.value);
878
+ 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' } });
879
+ return (_jsxs("button", { ...root, type: "button", role: "tab", "aria-selected": active, onClick: host.interactive ? e => { e.stopPropagation(); if (!dis && ctx)
880
+ 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] }));
881
+ }
882
+ function TabsPanel({ node, p, children }) {
883
+ const ctx = useContext(ValueContext);
884
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 12 } });
885
+ if (ctx && ctx.value !== str(p.value))
886
+ return null;
887
+ return (_jsx("div", { ...root, role: "tabpanel", children: children }));
888
+ }
889
+ function Breadcrumbs({ node, p, children }) {
890
+ const { t: i18n } = useI18n();
891
+ const sep = p.separator !== undefined ? str(p.separator) : '/';
892
+ const items = (Array.isArray(children) ? children : [children]).filter(Boolean);
893
+ const root = useRoot(node, p, { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, fontSize: 14 } });
894
+ 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))) }));
895
+ }
896
+ function Pagination({ node, p, disabled }) {
897
+ const total = Math.max(1, Number(p.total ?? 1) || 1);
898
+ const v = Number(useInputValue(node, p) ?? 1) || 1;
899
+ const pages = [];
900
+ for (let i = 1; i <= total; i++)
901
+ if (i === 1 || i === total || Math.abs(i - v) <= 1)
902
+ pages.push(i);
903
+ else if (pages[pages.length - 1] !== '…')
904
+ pages.push('…');
905
+ 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));
906
+ const root = useRoot(node, p, { disabled, style: { display: 'inline-flex', gap: 6, alignItems: 'center' } });
907
+ 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')] }));
908
+ }
909
+ function Stepper({ node, p, children }) {
910
+ const { state } = useCtx();
911
+ const active = Number(resolveValue(p.active, state) ?? 0);
912
+ const ids = (node.children ?? []).filter((c) => c.type === 'ui:Stepper.Step').map((c) => c.id);
913
+ const [content, setContent] = useState(null);
914
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 16 } });
915
+ 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 })] }) }));
916
+ }
917
+ function StepperStep({ node, p, children }) {
918
+ const seq = useContext(ItemProgressContext);
919
+ const i = seq ? seq.ids.indexOf(node.id) : 0;
920
+ const done = !!seq && i < seq.active;
921
+ const cur = !!seq && i === seq.active;
922
+ const last = !!seq && i === seq.ids.length - 1;
923
+ const root = useRoot(node, p, { style: { display: 'flex', alignItems: 'flex-start', gap: 8, flex: last ? '0 0 auto' : '1 1 0', minWidth: 0 } });
924
+ 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] }));
925
+ }
926
+ // ============ AI 交互 ============
927
+ function ChatMessage({ node, p, children }) {
928
+ const user = p.from === 'user';
929
+ const root = useRoot(node, p, { style: { display: 'flex', width: '100%', justifyContent: user ? 'flex-end' : 'flex-start' } });
930
+ 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] }) }));
931
+ }
932
+ const TOOL_STATE = (i18n) => ({
933
+ running: { text: i18n.stage.running, c: INFO },
934
+ done: { text: i18n.stage.done, c: OK },
935
+ error: { text: i18n.stage.error, c: DANGER },
936
+ 'input-streaming': { text: i18n.stage.preparing, c: DIMMED },
937
+ 'input-available': { text: i18n.stage.running, c: INFO },
938
+ 'output-available': { text: i18n.stage.done, c: OK },
939
+ 'output-error': { text: i18n.stage.error, c: DANGER },
940
+ });
941
+ function ToolCall({ node, p, children }) {
942
+ const { t: i18n } = useI18n();
943
+ const s = TOOL_STATE(i18n)[str(p.state, 'done')] ?? TOOL_STATE(i18n).done;
944
+ 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 } });
945
+ 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 })] }));
946
+ }
947
+ /**
948
+ * 思考过程(D56):标题是此刻在想什么;active 时标题闪动、前面转圈;结束后显示“思考了 {duration}”。
949
+ * opened 时展开子节点(过程),左侧一条细线。
950
+ */
951
+ function Thinking({ node, p, children }) {
952
+ const { t: i18n } = useI18n();
953
+ const active = p.active === true;
954
+ const opened = p.opened === true;
955
+ const title = str(p.title);
956
+ const duration = str(p.duration);
957
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%', fontSize: 14, color: DIMMED } });
958
+ 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] }));
959
+ }
960
+ /** 简单外部邮件:自身不可操作,正文里的按钮仍走普通步骤。 */
961
+ function Email({ node, p, children }) {
962
+ const { t: i18n } = useI18n();
963
+ const root = useRoot(node, p, { className: 'jbox', style: { background: SURFACE, padding: 16, display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 } });
964
+ const from = str(p.from, i18n.stage.sender);
965
+ 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 })] });
966
+ }
967
+ function DataCard({ node, p }) {
968
+ const { t: i18n } = useI18n();
969
+ const { changed = [] } = useCtx();
970
+ const rows = dataRows(p.value, p.fields, node.props?.value, changed);
971
+ const root = useRoot(node, p, { className: 'jbox', style: { background: SURFACE, padding: 20, minWidth: 0, width: '100%' } });
972
+ 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 })] });
973
+ }
974
+ function StateFlow({ node, p }) {
975
+ const { t: i18n } = useI18n();
976
+ const { changed = [] } = useCtx();
977
+ const items = stateFlowItems(p.value, p.states, p.labels);
978
+ const change = changeFor(node.props?.value, changed);
979
+ const label = (v) => items.find(item => item.value === v)?.label ?? displayData(v);
980
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', gap: 10 } });
981
+ 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] });
982
+ }
983
+ const ARTIFACT_ICON = { doc: '▤', code: '</>', sheet: '▦', web: '◎', image: '▣' };
984
+ const ARTIFACT_KIND = (i18n) => ({ doc: i18n.stage.document, code: i18n.stage.code, sheet: i18n.stage.sheet, web: i18n.stage.webPage, image: i18n.stage.image });
985
+ function ArtifactStatus({ status }) {
986
+ const { t: i18n } = useI18n();
987
+ if (status === 'generating')
988
+ 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] }));
989
+ if (status === 'error')
990
+ return _jsx("span", { style: { color: DANGER, flexShrink: 0, fontSize: 13 }, children: i18n.stage.generationFailed });
991
+ return _jsx("span", { style: { color: OK, flexShrink: 0, fontSize: 13 }, children: i18n.stage.generated });
992
+ }
993
+ /** Agent 的侧边产物(D56):标题栏(类型图标、标题、版本、状态)+ 内容区;默认占满所在容器的高度。 */
994
+ function Artifact({ node, p, children }) {
995
+ const { t: i18n } = useI18n();
996
+ const mode = useStroke();
997
+ const kind = str(p.kind, 'doc');
998
+ 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' } });
999
+ 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 })] }));
1000
+ }
1001
+ /** 对话里指向产物的卡片(可点击打开)。 */
1002
+ function ArtifactRef({ node, p, disabled }) {
1003
+ const { t: i18n } = useI18n();
1004
+ const mode = useStroke();
1005
+ const kind = str(p.kind, 'doc');
1006
+ 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' } });
1007
+ 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') })] })] }));
1008
+ }
1009
+ function PromptInput({ node, p, disabled }) {
1010
+ const { t: i18n } = useI18n();
1011
+ const ctx = useCtx();
1012
+ const v = useInputValue(node, p);
1013
+ const has = v !== undefined && v !== null && v !== '';
1014
+ 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 } });
1015
+ 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' } })] }));
1016
+ }
1017
+ // ============ 设备外框:终端与手机 ============
1018
+ const TERM_TONE = { plain: INK, ok: OK, warn: WARN, error: DANGER };
1019
+ const TERM_MONO = '[.jstage[data-stroke=clean]_&]:font-mono';
1020
+ /**
1021
+ * 终端(低保真线框,与其他组件同一风格):圆角窗口,左上角是 title;里面每条命令、每段输出各是一个带边框的圆角块。
1022
+ * 内容超出 h / mah 时在窗口内部滚动。tone 只改输出文字的颜色。
1023
+ */
1024
+ function Terminal({ node, p, children }) {
1025
+ // 像真实终端一样停在最新的输出(输入行可见)
1026
+ const body = useRef(null);
1027
+ useLayoutEffect(() => {
1028
+ const el = body.current;
1029
+ if (el)
1030
+ el.scrollTop = el.scrollHeight;
1031
+ });
1032
+ const root = useRoot(node, p, {
1033
+ className: 'jbox [.jstage[data-stroke=clean]_&]:rounded-[18px]',
1034
+ style: { display: 'flex', flexDirection: 'column', gap: 10, width: '100%', minHeight: 0, background: SURFACE, padding: '12px 16px 16px' },
1035
+ });
1036
+ 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 })] }));
1037
+ }
1038
+ function TerminalCommand({ node, p, children }) {
1039
+ const root = useRoot(node, p, {
1040
+ className: cn('jbox [.jstage[data-stroke=clean]_&]:rounded-[10px]', TERM_MONO),
1041
+ style: { display: 'flex', gap: 8, padding: '10px 16px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', background: SURFACE, flexShrink: 0 },
1042
+ });
1043
+ 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 })] }));
1044
+ }
1045
+ function TerminalOutput({ node, p, children }) {
1046
+ const tone = str(p.tone, 'plain');
1047
+ const root = useRoot(node, p, {
1048
+ className: cn('jbox [.jstage[data-stroke=clean]_&]:rounded-[14px]', TERM_MONO),
1049
+ style: { padding: '10px 16px', minHeight: 56, whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: 1.55, background: SURFACE, color: TERM_TONE[tone] ?? INK, flexShrink: 0 },
1050
+ });
1051
+ 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 })] }));
1052
+ }
1053
+ /** 终端里的输入行(值类型):与命令块同样的圆角块,type 动作写入;没有值时显示 placeholder 与光标。 */
1054
+ function TerminalInput({ node, p, disabled }) {
1055
+ const v = useInputValue(node, p);
1056
+ const has = v !== undefined && v !== null && v !== '';
1057
+ const root = useRoot(node, p, {
1058
+ disabled,
1059
+ className: cn('jbox [.jstage[data-stroke=clean]_&]:rounded-[10px]', TERM_MONO),
1060
+ style: { display: 'flex', gap: 8, alignItems: 'baseline', padding: '10px 16px', background: SURFACE, flexShrink: 0 },
1061
+ });
1062
+ 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 } })] })] }));
1063
+ }
1064
+ /** 手机外框的几何尺寸:机身边框,以及屏幕内的安全区(顶部避开听筒与圆角、底部避开圆角与横条、左右避开圆角)。 */
1065
+ export const PHONE_BEZEL = 12;
1066
+ export const PHONE_SAFE = { top: 40, bottom: 26, x: 12 };
1067
+ const PHONE_RADIUS = 54;
1068
+ const PHONE_BUTTONS = [
1069
+ { side: 'left', top: 118, h: 30 },
1070
+ { side: 'left', top: 176, h: 58 },
1071
+ { side: 'left', top: 246, h: 58 },
1072
+ { side: 'right', top: 196, h: 90 },
1073
+ ];
1074
+ /** 横放(刘海在左)时屏幕内的安全区:左侧避开刘海,右侧与上下避开圆角,底部避开横条。 */
1075
+ export const PHONE_SAFE_LANDSCAPE = { top: 12, bottom: 20, left: 44, right: 28 };
1076
+ const phoneInsets = (landscape) => landscape ? PHONE_SAFE_LANDSCAPE : { top: PHONE_SAFE.top, bottom: PHONE_SAFE.bottom, left: PHONE_SAFE.x, right: PHONE_SAFE.x };
1077
+ /** 外框占用的总尺寸(含侧键余量),内容区为 width × height;横放时侧键在机身上下沿。 */
1078
+ export function phoneFrameSize(width, height, landscape = false) {
1079
+ const s = phoneInsets(landscape);
1080
+ const screenW = width + s.left + s.right;
1081
+ const screenH = height + s.top + s.bottom;
1082
+ return landscape ? { width: screenW + PHONE_BEZEL * 2, height: screenH + PHONE_BEZEL * 2 + 8 } : { width: screenW + PHONE_BEZEL * 2 + 8, height: screenH + PHONE_BEZEL * 2 };
1083
+ }
1084
+ /**
1085
+ * 手机外框(Phone 组件与舞台的自动外框共用):大圆角机身、侧边按键、顶部听筒、底部横条,不显示任何文字。
1086
+ * children 放在屏幕安全区以内的内容区,内容区正好是 width × height(断点按 width 计算,不被圆角和听筒遮挡);
1087
+ * scale 只做视觉缩放。主题取外层 ThemeContext(sketch 用手绘描边)。
1088
+ */
1089
+ export function PhoneFrame({ width, height, scale = 1, children, seed = 'phone', landscape = false }) {
1090
+ const mode = useStroke();
1091
+ const { theme, font, style: fontStyle } = useLook();
1092
+ const sketch = mode === 'rough';
1093
+ const safe = phoneInsets(landscape);
1094
+ const screenW = width + safe.left + safe.right;
1095
+ const screenH = height + safe.top + safe.bottom;
1096
+ const W = screenW + PHONE_BEZEL * 2;
1097
+ const H = screenH + PHONE_BEZEL * 2;
1098
+ const ink = INK;
1099
+ const size = phoneFrameSize(width, height, landscape);
1100
+ // 竖放:侧键在左右两侧,机身左移 4 留出左侧键;横放(逆时针转 90°):左侧键转到下沿、右侧键转到上沿,机身下移 4
1101
+ const buttons = PHONE_BUTTONS.map((b) => landscape
1102
+ ? { edge: (b.side === 'left' ? 'bottom' : 'top'), at: b.side === 'left' ? b.top : W - b.top - b.h, len: b.h }
1103
+ : { edge: b.side, at: b.top, len: b.h });
1104
+ 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'
1105
+ ? { 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' }
1106
+ : { 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
1107
+ ? _jsx("div", { "aria-hidden": true, style: { position: 'absolute', left: 12, top: '50%', width: 24, height: 96, marginTop: -48, borderRadius: 12, background: ink } })
1108
+ : _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
1109
+ ? _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}` } })
1110
+ : _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] }) }));
1111
+ }
1112
+ /** Phone 组件:子节点放进手机屏幕,按手机宽度排版(响应式断点按这个宽度计算);弹窗渲染在屏幕内。 */
1113
+ function Phone({ node, p, children }) {
1114
+ const ctx = useCtx();
1115
+ const [layer, setLayer] = useState(null);
1116
+ const r = useR(p);
1117
+ const landscape = p.landscape === true;
1118
+ // 横放:内容区按横屏手机的尺寸排版(断点按这个宽度算);h 仍可改高度
1119
+ const width = childWidth(node, ctx.width, p);
1120
+ const hv = Number(r('h'));
1121
+ const height = Number.isFinite(hv) && hv > 0 ? hv : landscape ? PHONE_LANDSCAPE.height : VIEWPORT_SIZE.mobile.height;
1122
+ const sv = Number(r('scale'));
1123
+ const scale = Number.isFinite(sv) && sv > 0 ? sv : 1;
1124
+ const { h: _h, w: _w, landscape: _l, ...rest } = p;
1125
+ const root = useRoot(node, rest, { style: { display: 'inline-block', flexShrink: 0 } });
1126
+ 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" })] }) }));
1127
+ }
1128
+ // ============ Joplo ============
1129
+ function Placeholder({ node, p }) {
1130
+ const { t: i18n } = useI18n();
1131
+ const h = dim(p.h ?? p.height) ?? '120px';
1132
+ const root = useRoot(node, p, { className: 'jbox', style: { display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'center', background: SUBTLE, fontSize: 14, color: DIMMED, height: h } });
1133
+ 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) })] }));
1134
+ }
1135
+ // ============ 产品界面(D68):官网示例里反复手拼的常见界面 ============
1136
+ // 都只读主题变量(--jp-*)与 Rough 描边;状态全部来自绑定的属性 / state.inputs,由步骤驱动;不请求网络、不加载外部资源。
1137
+ const ROW_GAP = 12;
1138
+ /** Avatar.Group:头像叠在一起(负间距 + 底色描边圈)。 */
1139
+ function AvatarGroup({ node, p, children }) {
1140
+ const overlap = typeof p.spacing === 'number' ? p.spacing : (SPACING[str(p.spacing, 'sm')] ?? 12) * 0.7;
1141
+ const root = useRoot(node, p, { className: 'javatar-group', style: { display: 'inline-flex', alignItems: 'center', ['--javatar-overlap']: `${-Math.round(overlap)}px` } });
1142
+ return _jsx("div", { ...root, children: children });
1143
+ }
1144
+ /** Radio.Card:Radio.Group 里的可选卡片;是否选中读 Radio.Group 的当前值。 */
1145
+ function RadioCard({ node, p, disabled }) {
1146
+ const host = useCtx();
1147
+ const ctx = useContext(ValueContext);
1148
+ const standalone = useInputValue(node, p) === true;
1149
+ const on = ctx ? ctx.value !== null && ctx.value === str(p.value) : standalone;
1150
+ 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 } });
1151
+ return (_jsxs("div", { ...root, role: "radio", tabIndex: host.interactive ? 0 : undefined, onClick: host.interactive ? e => { e.stopPropagation(); if (!disabled && ctx && !ctx.disabled)
1152
+ 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') {
1153
+ e.preventDefault();
1154
+ e.stopPropagation();
1155
+ e.currentTarget.click();
1156
+ } } : 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] })] }));
1157
+ }
1158
+ /** EmptyState:空列表、404 / 过期 / 无权限页、结果页。图标圈 + 标题 + 说明 + 子节点里的操作按钮。 */
1159
+ function EmptyState({ node, p, children }) {
1160
+ const c = color(p.color) ?? INK2;
1161
+ const border = p.withBorder === true;
1162
+ 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' } });
1163
+ 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] }));
1164
+ }
1165
+ /** ListRow:头像 / 图标 + 标题 + 副标题,右侧内容放在子节点。active 时浅色底。 */
1166
+ function ListRow({ node, p, children }) {
1167
+ const c = color(p.color) ?? INK2;
1168
+ const active = p.active === true;
1169
+ const border = p.withBorder === true;
1170
+ 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 } });
1171
+ 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] }));
1172
+ }
1173
+ /** diff 的颜色:以 - / − 开头为红,以 + 开头为绿,其余跟随 color。 */
1174
+ export function diffTone(diff) {
1175
+ const t = diff.trim();
1176
+ if (/^[-−]/.test(t))
1177
+ return 'down';
1178
+ if (/^\+/.test(t))
1179
+ return 'up';
1180
+ return 'flat';
1181
+ }
1182
+ /** Stat:指标卡。 */
1183
+ function Stat({ node, p }) {
1184
+ const c = color(p.color) ?? INK;
1185
+ const diff = p.diff !== undefined && p.diff !== null && p.diff !== '' ? str(p.diff) : null;
1186
+ const tone = diff ? diffTone(diff) : 'flat';
1187
+ const progress = typeof p.progress === 'number' && Number.isFinite(p.progress) ? Math.max(0, Math.min(100, p.progress)) : null;
1188
+ 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 } });
1189
+ 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] }));
1190
+ }
1191
+ /** KeyValue:一列“名称 …… 值”;total 时最后一行加粗并在上方画分隔线。 */
1192
+ function KeyValue({ node, p }) {
1193
+ const items = arr(p.items).filter((x) => !!x && typeof x === 'object');
1194
+ const root = useRoot(node, p, { style: { display: 'flex', flexDirection: 'column', width: '100%' } });
1195
+ return (_jsx("dl", { ...root, style: { ...root.style, margin: 0 }, children: items.map((it, i) => {
1196
+ const last = i === items.length - 1;
1197
+ const total = p.total === true && last && items.length > 1;
1198
+ 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));
1199
+ }) }));
1200
+ }
1201
+ // ---------- 图表(BarChart / LineChart,对齐 @mantine/charts 的 data / dataKey / series) ----------
1202
+ const SERIES_COLORS = [INFO, ACCENT, OK, DANGER, '#7048e8', '#0ca678'];
1203
+ /** 图表的纯数据部分:每个系列取 data[i][name](不是数字的当作 0),刻度取“好看”的整数步长。 */
1204
+ export function chartModel(data, dataKey, series) {
1205
+ const rows = arr(data).filter((r) => !!r && typeof r === 'object');
1206
+ const key = str(dataKey);
1207
+ const list = arr(series)
1208
+ .filter((s) => !!s && typeof s === 'object' && typeof s.name === 'string')
1209
+ .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)) }));
1210
+ const peak = Math.max(0, ...list.flatMap((s) => s.values));
1211
+ const step = niceStep(peak / 4);
1212
+ const max = Math.max(step, Math.ceil(peak / step) * step);
1213
+ const ticks = Array.from({ length: Math.round(max / step) + 1 }, (_, i) => i * step);
1214
+ return { labels: rows.map((r) => str(r[key])), series: list, max, ticks };
1215
+ }
1216
+ function niceStep(raw) {
1217
+ if (!(raw > 0))
1218
+ return 1;
1219
+ const pow = 10 ** Math.floor(Math.log10(raw));
1220
+ const f = raw / pow;
1221
+ return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * pow;
1222
+ }
1223
+ function Chart({ node, p, kind }) {
1224
+ const { animate } = useCtx();
1225
+ const r = useR(p);
1226
+ const m = chartModel(p.data, p.dataKey, p.series);
1227
+ const H = typeof r('h') === 'number' ? r('h') : 220;
1228
+ const W = 600;
1229
+ const pad = { l: 40, r: 12, t: 12, b: 28 };
1230
+ const iw = W - pad.l - pad.r;
1231
+ const legend = (m.series.length > 1 && p.withLegend !== false) || p.withLegend === true;
1232
+ // h 是整张图(含图例)的高度:图例占 22px,其余给 SVG
1233
+ const svgH = Math.max(80, H - (legend ? 22 : 0));
1234
+ const ih = svgH - pad.t - pad.b;
1235
+ const unit = p.unit !== undefined ? str(p.unit) : '';
1236
+ const n = Math.max(1, m.labels.length);
1237
+ const band = iw / n;
1238
+ const y = (v) => pad.t + ih - (v / m.max) * ih;
1239
+ const root = useRoot(node, p, { style: { width: '100%', position: 'relative' } });
1240
+ 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'
1241
+ ? m.series.map((s, si) => {
1242
+ const bw = Math.max(4, (band * 0.7) / m.series.length);
1243
+ return s.values.map((v, i) => {
1244
+ const x = pad.l + band * i + band * 0.15 + bw * si;
1245
+ 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}`);
1246
+ });
1247
+ })
1248
+ : m.series.map((s, si) => {
1249
+ const pts = s.values.map((v, i) => [pad.l + band * (i + 0.5), y(v)]);
1250
+ const d = pts.map(([x, yy], i) => `${i ? 'L' : 'M'}${x.toFixed(1)},${yy.toFixed(1)}`).join(' ');
1251
+ 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));
1252
+ })] }), 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] }));
1253
+ }
1254
+ const BarChart = (props) => _jsx(Chart, { ...props, kind: "bar" });
1255
+ const LineChart = (props) => _jsx(Chart, { ...props, kind: "line" });
1256
+ // ---------- Menu(对齐 Mantine):opened 写在 Menu 上,编译器抄给 Menu.Dropdown(core 门控) ----------
1257
+ const MenuContext = createContext(null);
1258
+ function Menu({ node, p, children }) {
1259
+ const anchor = useRef(null);
1260
+ const root = useRoot(node, p, { style: { position: 'relative', display: 'inline-block' } });
1261
+ return (_jsx(MenuContext.Provider, { value: { anchor, position: str(p.position, 'bottom-start') }, children: _jsx("div", { ...root, ref: anchor, children: children }) }));
1262
+ }
1263
+ function MenuTarget({ node, p, children }) {
1264
+ const root = useRoot(node, p, { style: { display: 'inline-flex' } });
1265
+ return _jsx("span", { ...root, children: children });
1266
+ }
1267
+ /** 下拉相对 Menu 的位置:top-* 在上方,*-end 右对齐。 */
1268
+ export function menuPlacement(position) {
1269
+ return { top: position.startsWith('top'), end: position.endsWith('end') };
1270
+ }
1271
+ function MenuDropdown({ node, p, children }) {
1272
+ const { dialogLayer, animate } = useCtx();
1273
+ const menu = useContext(MenuContext);
1274
+ const place = menuPlacement(menu?.position ?? 'bottom-start');
1275
+ const [pos, setPos] = useState(null);
1276
+ // 浮到弹窗层(与 Modal 同一层,跟着镜头一起平移缩放),不被表格、卡片的 overflow 裁掉;坐标按镜头缩放换回画面坐标
1277
+ useLayoutEffect(() => {
1278
+ const a = menu?.anchor.current;
1279
+ if (!a || !dialogLayer)
1280
+ return;
1281
+ const ar = rectOf(a);
1282
+ const lr = rectOf(dialogLayer);
1283
+ const k = dialogLayer.offsetWidth ? lr.width / dialogLayer.offsetWidth : 1;
1284
+ 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) };
1285
+ if (!pos || Math.abs(pos.left - next.left) > 0.5 || Math.abs(pos.top - next.top) > 0.5)
1286
+ setPos(next);
1287
+ });
1288
+ const shift = `${place.end ? 'translateX(-100%)' : ''} ${place.top ? 'translateY(-100%)' : ''}`.trim() || undefined;
1289
+ 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' } });
1290
+ 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] }));
1291
+ if (dialogLayer && pos)
1292
+ return createPortal(_jsx("div", { style: { position: 'absolute', left: pos.left, top: pos.top, transform: shift, zIndex: 5 }, children: panel }), dialogLayer);
1293
+ // 还没测量(首帧、服务端渲染的截图):先在 Menu 旁边原位浮出
1294
+ 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 });
1295
+ }
1296
+ function MenuItem({ node, p, disabled, children }) {
1297
+ const c = color(p.color);
1298
+ 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' } });
1299
+ 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] }));
1300
+ }
1301
+ function MenuLabel({ node, p, children }) {
1302
+ const root = useRoot(node, p, { style: { padding: '6px 10px 4px', fontSize: 12, fontWeight: 600, color: DIMMED } });
1303
+ return _jsx("div", { ...root, children: children });
1304
+ }
1305
+ function MenuDivider({ node, p }) {
1306
+ const root = useRoot(node, p, { style: { height: 1, background: LINE, margin: '4px 6px' } });
1307
+ return _jsx("div", { ...root, role: "separator" });
1308
+ }
1309
+ /** TabBar:手机底部标签栏;当前值来自 state.inputs(select 写入)或 value / defaultValue。 */
1310
+ function TabBar({ node, p, disabled }) {
1311
+ const { state, preview } = useCtx();
1312
+ const v = useInputValue(node, p);
1313
+ const items = arr(p.data).map((d) => (d && typeof d === 'object' ? d : { value: d, label: d }));
1314
+ const choices = choicesOf(node, state) ?? [];
1315
+ const pv = preview && preview.nodeId === node.id ? preview.text : null;
1316
+ 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);
1317
+ const c = color(p.color) ?? INK;
1318
+ 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' } });
1319
+ 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) => {
1320
+ const value = str(it.value);
1321
+ const on = value === cur;
1322
+ const badge = it.badge !== undefined && it.badge !== null && it.badge !== '' && it.badge !== 0 ? str(it.badge) : null;
1323
+ 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}`));
1324
+ }) }));
1325
+ }
1326
+ // ============ 注册表:目录中每个类型都必须有实现(Record<ComponentType, …> 由类型检查保证) ============
1327
+ const IMPL = {
1328
+ AppShell,
1329
+ 'AppShell.Header': ShellHeader,
1330
+ 'AppShell.Navbar': ShellSide,
1331
+ 'AppShell.Main': ShellMain,
1332
+ 'AppShell.Aside': ShellSide,
1333
+ 'AppShell.Footer': ShellFooter,
1334
+ Container,
1335
+ Stack,
1336
+ Group,
1337
+ Flex,
1338
+ Grid,
1339
+ 'Grid.Col': GridCol,
1340
+ SimpleGrid,
1341
+ Center,
1342
+ Box,
1343
+ Space,
1344
+ Paper,
1345
+ Card,
1346
+ 'Card.Section': CardSection,
1347
+ Divider,
1348
+ ScrollArea,
1349
+ Title,
1350
+ Text,
1351
+ Code,
1352
+ Kbd,
1353
+ Blockquote,
1354
+ List,
1355
+ 'List.Item': ListItem,
1356
+ Badge,
1357
+ Avatar,
1358
+ Image: ImageBox,
1359
+ ThemeIcon,
1360
+ Indicator,
1361
+ Timeline,
1362
+ 'Timeline.Item': TimelineItem,
1363
+ Table,
1364
+ 'Table.Thead': tablePart('thead'),
1365
+ 'Table.Tbody': tablePart('tbody'),
1366
+ 'Table.Tr': tablePart('tr'),
1367
+ 'Table.Th': tablePart('th'),
1368
+ 'Table.Td': tablePart('td'),
1369
+ DataTable,
1370
+ Accordion,
1371
+ 'Accordion.Item': AccordionItem,
1372
+ 'Accordion.Panel': AccordionPanel,
1373
+ Radio,
1374
+ 'Tabs.List': TabsList,
1375
+ 'Tabs.Panel': TabsPanel,
1376
+ Breadcrumbs,
1377
+ Stepper,
1378
+ 'Stepper.Step': StepperStep,
1379
+ Alert,
1380
+ Loader,
1381
+ Progress,
1382
+ RingProgress,
1383
+ Skeleton,
1384
+ Notification,
1385
+ Modal: Overlay,
1386
+ Drawer: Overlay,
1387
+ ChatMessage,
1388
+ ToolCall,
1389
+ Thinking,
1390
+ Email,
1391
+ DataCard,
1392
+ StateFlow,
1393
+ Artifact,
1394
+ Terminal,
1395
+ 'Terminal.Command': TerminalCommand,
1396
+ 'Terminal.Output': TerminalOutput,
1397
+ Phone,
1398
+ ...Object.fromEntries(HTML_TAGS.map((t) => [t, htmlImpl(t)])),
1399
+ Placeholder,
1400
+ // 产品界面(D68)
1401
+ 'Avatar.Group': AvatarGroup,
1402
+ 'Radio.Card': RadioCard,
1403
+ EmptyState,
1404
+ ListRow,
1405
+ Stat,
1406
+ KeyValue,
1407
+ BarChart,
1408
+ LineChart,
1409
+ Menu,
1410
+ 'Menu.Target': MenuTarget,
1411
+ 'Menu.Dropdown': MenuDropdown,
1412
+ 'Menu.Item': MenuItem,
1413
+ 'Menu.Label': MenuLabel,
1414
+ 'Menu.Divider': MenuDivider,
1415
+ TabBar,
1416
+ Button,
1417
+ ActionIcon,
1418
+ CloseButton,
1419
+ Anchor,
1420
+ a: Anchor,
1421
+ NavLink,
1422
+ Burger,
1423
+ TextInput,
1424
+ PasswordInput: TextInput,
1425
+ NumberInput: TextInput,
1426
+ Textarea,
1427
+ Select,
1428
+ MultiSelect: Select,
1429
+ Checkbox,
1430
+ 'Radio.Group': RadioGroup,
1431
+ Switch,
1432
+ SegmentedControl,
1433
+ FileInput: TextInput,
1434
+ Tabs,
1435
+ 'Tabs.Tab': TabsTab,
1436
+ 'Accordion.Control': AccordionControl,
1437
+ Pagination,
1438
+ PromptInput,
1439
+ 'Artifact.Ref': ArtifactRef,
1440
+ 'Terminal.Input': TerminalInput,
1441
+ };
1442
+ export const IMPLEMENTED_TYPES = Object.keys(IMPL).map(name => `ui:${name}`);
1443
+ export default defineRuntime({
1444
+ id: 'ui',
1445
+ 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() }) }])),
1446
+ viewports: { mobile: { insets: { x: 4 + PHONE_BEZEL + PHONE_SAFE.x, y: PHONE_BEZEL + PHONE_SAFE.top }, size: phoneFrameSize, react: PhoneFrame } },
1447
+ });