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