anentrypoint-design 0.0.381 → 0.0.382
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/dist/247420.css +228 -162
- package/dist/247420.js +24 -24
- package/package.json +1 -1
- package/src/components/content/avatar.js +31 -0
- package/src/components/content/charts.js +50 -0
- package/src/components/content/cli.js +46 -0
- package/src/components/content/feedback.js +77 -0
- package/src/components/content/fields.js +136 -0
- package/src/components/content/hero.js +146 -0
- package/src/components/content/lists.js +85 -0
- package/src/components/content/panel.js +79 -0
- package/src/components/content/row.js +115 -0
- package/src/components/content/table.js +97 -0
- package/src/components/content/views.js +81 -0
- package/src/components/content.js +29 -861
- package/src/components/editor-primitives/batch.js +60 -0
- package/src/components/editor-primitives/chrome.js +146 -0
- package/src/components/editor-primitives/collapse.js +46 -0
- package/src/components/editor-primitives/context-menu.js +127 -0
- package/src/components/editor-primitives/diagnostics.js +44 -0
- package/src/components/editor-primitives/focus-trap.js +26 -0
- package/src/components/editor-primitives/json-viewer.js +123 -0
- package/src/components/editor-primitives/layout.js +89 -0
- package/src/components/editor-primitives/modals.js +89 -0
- package/src/components/editor-primitives/pager.js +74 -0
- package/src/components/editor-primitives/property-grid.js +57 -0
- package/src/components/editor-primitives/shared.js +18 -0
- package/src/components/editor-primitives/split-panel.js +101 -0
- package/src/components/editor-primitives/toast.js +59 -0
- package/src/components/editor-primitives/tree.js +75 -0
- package/src/components/editor-primitives.js +35 -1048
- package/src/components/overlay-primitives/approval-prompt.js +38 -0
- package/src/components/overlay-primitives/auth-modal.js +89 -0
- package/src/components/overlay-primitives/command-palette.js +101 -0
- package/src/components/overlay-primitives/emoji-picker.js +103 -0
- package/src/components/overlay-primitives/floating.js +146 -0
- package/src/components/overlay-primitives/full-screen.js +45 -0
- package/src/components/overlay-primitives/menus.js +131 -0
- package/src/components/overlay-primitives/popover.js +51 -0
- package/src/components/overlay-primitives/roving-menu.js +73 -0
- package/src/components/overlay-primitives/settings-popover.js +85 -0
- package/src/components/overlay-primitives/tooltip.js +55 -0
- package/src/components/overlay-primitives.js +33 -855
- package/src/css/app-shell/chat-polish.css +39 -32
- package/src/css/app-shell/data-density.css +25 -21
- package/src/css/app-shell/files.css +37 -29
- package/src/css/app-shell/kits-appended.css +73 -50
- package/src/css/app-shell/responsive.css +47 -29
- package/src/kits/os/freddie-dashboard.css +2 -2
|
@@ -3,1052 +3,39 @@
|
|
|
3
3
|
// visuals route through CSS classes defined in editor-primitives.css;
|
|
4
4
|
// no hex/rgba literals appear in this file. Theme switching happens
|
|
5
5
|
// via the kit's data-theme attribute on the .ds-247420 scope root.
|
|
6
|
-
|
|
7
|
-
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
8
|
-
import { Icon } from './shell.js';
|
|
9
|
-
const h = webjsx.createElement;
|
|
10
|
-
|
|
11
|
-
function kids(c) { return c == null ? [] : (Array.isArray(c) ? c : [c]); }
|
|
12
|
-
|
|
13
|
-
export function Toolbar({ leading = [], trailing = [], dense = false, children } = {}) {
|
|
14
|
-
const cls = 'ds-ep-toolbar' + (dense ? ' dense' : '');
|
|
15
|
-
return h('div', { class: cls, role: 'toolbar' },
|
|
16
|
-
h('div', { class: 'ds-ep-toolbar-leading' }, ...kids(leading)),
|
|
17
|
-
children != null ? h('div', { class: 'ds-ep-toolbar-center' }, ...kids(children)) : null,
|
|
18
|
-
h('div', { class: 'ds-ep-toolbar-trailing' }, ...kids(trailing))
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// ToolbarRow — a flat, wrapping row of arbitrary action nodes (buttons,
|
|
24
|
-
// inputs, chips) with no leading/center/trailing slot structure. Toolbar's
|
|
25
|
-
// three-slot split is the wrong shape when a caller just wants "this row of
|
|
26
|
-
// controls, left to right, wrapping on narrow viewports" — the exact shape
|
|
27
|
-
// gmsniff's panels.js hand-rolled as a bare '.gm-toolbar' div because Toolbar
|
|
28
|
-
// didn't cover it. Accepts children as varargs or a single array.
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
export function ToolbarRow(...actions) {
|
|
31
|
-
const flat = actions.length === 1 && Array.isArray(actions[0]) ? actions[0] : actions;
|
|
32
|
-
return h('div', { class: 'ds-ep-toolbar-row', role: 'toolbar' }, ...kids(flat));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function Tabs({ items = [], active, onChange, children, 'aria-label': ariaLabel, onClose, scroll = false } = {}) {
|
|
36
|
-
// Roving tabindex + arrow nav per WAI-ARIA tabs pattern.
|
|
37
|
-
// Only the active tab is in the tab order; arrows move focus + activate.
|
|
38
|
-
const activeIdx = Math.max(0, items.findIndex(it => it.id === active));
|
|
39
|
-
const onTabKeyDown = (e, idx) => {
|
|
40
|
-
let next = null;
|
|
41
|
-
if (e.key === 'ArrowRight') next = (idx + 1) % items.length;
|
|
42
|
-
else if (e.key === 'ArrowLeft') next = (idx - 1 + items.length) % items.length;
|
|
43
|
-
else if (e.key === 'Home') next = 0;
|
|
44
|
-
else if (e.key === 'End') next = items.length - 1;
|
|
45
|
-
if (next == null) return;
|
|
46
|
-
e.preventDefault();
|
|
47
|
-
const nextId = items[next]?.id;
|
|
48
|
-
if (nextId && onChange) onChange(nextId);
|
|
49
|
-
// Move focus on next paint (so the newly rendered active button gets focus)
|
|
50
|
-
queueMicrotask(() => {
|
|
51
|
-
const head = e.currentTarget?.parentElement;
|
|
52
|
-
const btn = head?.querySelectorAll('[role="tab"]')[next];
|
|
53
|
-
if (btn) btn.focus();
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
// Position the sliding underline from the active tab's geometry. The ref is
|
|
57
|
-
// on the OUTER .ds-ep-tabs (position:relative) — the indicator is its child,
|
|
58
|
-
// NOT the flex head's (an abspos child of a horizontal flex row mis-sizes to
|
|
59
|
-
// 0 in Chromium). Runs on every render (ref fires after applyDiff) so a tab
|
|
60
|
-
// change re-measures; the CSS transition animates the move. left/width come
|
|
61
|
-
// from the active tab; top sits the bar on the head's bottom edge.
|
|
62
|
-
const positionSlider = (root) => {
|
|
63
|
-
if (!root) return;
|
|
64
|
-
const head = root.querySelector('.ds-ep-tabs-head');
|
|
65
|
-
const active = root.querySelector('.ds-ep-tab.active');
|
|
66
|
-
const ind = root.querySelector('.ds-ep-tab-indicator');
|
|
67
|
-
if (!head || !active || !ind) return;
|
|
68
|
-
// Size via left+right insets, NOT width: an abspos element's inline
|
|
69
|
-
// `width` computes to 0 in some flex-sibling layouts (Chromium), but
|
|
70
|
-
// left+right insets size reliably. left/right animate via the CSS
|
|
71
|
-
// transition on .ds-ep-tab-indicator.
|
|
72
|
-
const rootW = root.offsetWidth;
|
|
73
|
-
const l = active.offsetLeft, w = active.offsetWidth;
|
|
74
|
-
ind.style.left = l + 'px';
|
|
75
|
-
ind.style.right = Math.max(0, rootW - l - w) + 'px';
|
|
76
|
-
ind.style.top = (head.offsetTop + head.offsetHeight - 2) + 'px';
|
|
77
|
-
head.classList.add('has-slider');
|
|
78
|
-
};
|
|
79
|
-
// scroll=true: tabs size to content (min/max-width) instead of stretching
|
|
80
|
-
// equally (flex:1) — the shape pi-web's TabBar uses for an open-file strip
|
|
81
|
-
// where tab count is unbounded and overflow-x scroll (already on
|
|
82
|
-
// .ds-ep-tabs-head) needs real per-tab widths to have something to scroll.
|
|
83
|
-
// onClose: per-item close affordance — a close button plus middle-click
|
|
84
|
-
// (auxclick button 1) to close, matching pi-web's TabBar. Opt-in: passing
|
|
85
|
-
// onClose without scroll still renders close buttons on the flex:1 tabs.
|
|
86
|
-
const closable = typeof onClose === 'function';
|
|
87
|
-
return h('div', { class: 'ds-ep-tabs', ref: positionSlider },
|
|
88
|
-
h('div', { class: 'ds-ep-tabs-head' + (scroll ? ' scroll' : ''), role: 'tablist', 'aria-label': ariaLabel || 'tabs' },
|
|
89
|
-
...items.map((it, idx) => h('span', {
|
|
90
|
-
key: it.id,
|
|
91
|
-
class: 'ds-ep-tab-wrap' + (it.id === active ? ' active' : ''),
|
|
92
|
-
onmousedown: closable ? (e) => { if (e.button === 1) e.preventDefault(); } : null,
|
|
93
|
-
onauxclick: closable ? (e) => {
|
|
94
|
-
if (e.button !== 1) return;
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
e.stopPropagation();
|
|
97
|
-
onClose(it.id);
|
|
98
|
-
} : null
|
|
99
|
-
},
|
|
100
|
-
h('button', {
|
|
101
|
-
type: 'button',
|
|
102
|
-
class: 'ds-ep-tab' + (it.id === active ? ' active' : ''),
|
|
103
|
-
role: 'tab',
|
|
104
|
-
id: 'tab-' + it.id,
|
|
105
|
-
title: typeof it.label === 'string' ? it.label : undefined,
|
|
106
|
-
'aria-selected': it.id === active ? 'true' : 'false',
|
|
107
|
-
'aria-controls': 'tabpanel-' + it.id,
|
|
108
|
-
'aria-label': typeof it.label === 'string' ? it.label : ('tab ' + (idx + 1)),
|
|
109
|
-
tabindex: idx === activeIdx ? '0' : '-1',
|
|
110
|
-
onclick: () => onChange && onChange(it.id),
|
|
111
|
-
onkeydown: (e) => onTabKeyDown(e, idx)
|
|
112
|
-
}, it.label),
|
|
113
|
-
closable ? h('button', {
|
|
114
|
-
type: 'button',
|
|
115
|
-
class: 'ds-ep-tab-close',
|
|
116
|
-
title: 'Close',
|
|
117
|
-
'aria-label': 'Close ' + (typeof it.label === 'string' ? it.label : 'tab'),
|
|
118
|
-
onclick: (e) => { e.stopPropagation(); onClose(it.id); }
|
|
119
|
-
}, Icon('x', { size: 14 })) : null
|
|
120
|
-
))
|
|
121
|
-
),
|
|
122
|
-
// The sliding underline — child of the outer column (see positionSlider).
|
|
123
|
-
// Keyed + decorative. Renders at 0-width until positioned (no-JS: hidden).
|
|
124
|
-
h('span', { key: '__ind', class: 'ds-ep-tab-indicator', 'aria-hidden': 'true' }),
|
|
125
|
-
h('div', {
|
|
126
|
-
class: 'ds-ep-tabs-body',
|
|
127
|
-
role: 'tabpanel',
|
|
128
|
-
id: active ? 'tabpanel-' + active : undefined,
|
|
129
|
-
'aria-labelledby': active ? 'tab-' + active : undefined,
|
|
130
|
-
tabindex: '0'
|
|
131
|
-
}, ...kids(children))
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function TreeView({ children } = {}) {
|
|
136
|
-
return h('div', { class: 'ds-ep-tree', role: 'tree' }, ...kids(children));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function TreeItem({ label, glyph, tag, depth = 0, selected = false, expanded = false, onSelect, onToggle, children, hasChildren } = {}) {
|
|
140
|
-
// Support legacy 'hasChildren' prop for future; infer from children param
|
|
141
|
-
const hasKids = hasChildren != null ? hasChildren : (children != null);
|
|
142
|
-
// Tree keyboard model (WAI-ARIA): Up/Down move between visible rows, Right expands/enters,
|
|
143
|
-
// Left collapses/moves to parent, Enter/Space activate, Home/End jump to first/last visible.
|
|
144
|
-
const onRowKeyDown = (e) => {
|
|
145
|
-
const row = e.currentTarget;
|
|
146
|
-
const tree = row.closest('[role="tree"]');
|
|
147
|
-
if (!tree) return;
|
|
148
|
-
const rows = Array.from(tree.querySelectorAll('.ds-ep-tree-row'));
|
|
149
|
-
const idx = rows.indexOf(row);
|
|
150
|
-
if (idx < 0) return;
|
|
151
|
-
const move = (i) => {
|
|
152
|
-
const r = rows[Math.max(0, Math.min(rows.length - 1, i))];
|
|
153
|
-
if (r) r.focus();
|
|
154
|
-
};
|
|
155
|
-
switch (e.key) {
|
|
156
|
-
case 'ArrowDown': e.preventDefault(); move(idx + 1); break;
|
|
157
|
-
case 'ArrowUp': e.preventDefault(); move(idx - 1); break;
|
|
158
|
-
case 'Home': e.preventDefault(); move(0); break;
|
|
159
|
-
case 'End': e.preventDefault(); move(rows.length - 1); break;
|
|
160
|
-
case 'ArrowRight':
|
|
161
|
-
if (hasKids && !expanded && onToggle) { e.preventDefault(); onToggle(); }
|
|
162
|
-
else if (hasKids && expanded) { e.preventDefault(); move(idx + 1); }
|
|
163
|
-
break;
|
|
164
|
-
case 'ArrowLeft':
|
|
165
|
-
if (hasKids && expanded && onToggle) { e.preventDefault(); onToggle(); }
|
|
166
|
-
break;
|
|
167
|
-
case 'Enter':
|
|
168
|
-
case ' ':
|
|
169
|
-
e.preventDefault();
|
|
170
|
-
if (onSelect) onSelect();
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
return h('div', {
|
|
175
|
-
class: 'ds-ep-tree-item' + (selected ? ' selected' : ''),
|
|
176
|
-
role: 'treeitem',
|
|
177
|
-
'aria-selected': selected ? 'true' : 'false',
|
|
178
|
-
'aria-expanded': hasKids ? String(!!expanded) : null,
|
|
179
|
-
'aria-level': depth + 1
|
|
180
|
-
},
|
|
181
|
-
h('div', {
|
|
182
|
-
class: 'ds-ep-tree-row',
|
|
183
|
-
style: 'padding-left:calc(' + depth + ' * var(--tree-indent,12px) + var(--tree-base-indent,6px))',
|
|
184
|
-
tabindex: selected ? '0' : '-1',
|
|
185
|
-
onclick: () => onSelect && onSelect(),
|
|
186
|
-
onkeydown: onRowKeyDown
|
|
187
|
-
},
|
|
188
|
-
h('span', {
|
|
189
|
-
class: 'ds-ep-tree-twist' + (expanded ? ' open' : ''),
|
|
190
|
-
'aria-hidden': 'true',
|
|
191
|
-
onclick: (e) => { e.stopPropagation(); if (hasKids && onToggle) onToggle(); }
|
|
192
|
-
}, hasKids ? Icon('chevron-right') : ''),
|
|
193
|
-
glyph != null ? h('span', { class: 'ds-ep-tree-glyph', 'aria-hidden': 'true' }, glyph) : null,
|
|
194
|
-
h('span', { class: 'ds-ep-tree-label' }, label),
|
|
195
|
-
tag != null ? h('span', { class: 'ds-ep-tree-tag' }, tag) : null
|
|
196
|
-
),
|
|
197
|
-
hasKids && expanded ? h('div', { class: 'ds-ep-tree-children', role: 'group' }, ...kids(children)) : null
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function PropertyGrid({ children } = {}) {
|
|
202
|
-
return h('div', { class: 'ds-ep-propgrid', role: 'group' }, ...kids(children));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function PropertyField({ label, hint, inline = false, children } = {}) {
|
|
206
|
-
return h('label', { class: 'ds-ep-propfield' + (inline ? ' inline' : '') },
|
|
207
|
-
h('span', { class: 'ds-ep-propfield-label' }, label),
|
|
208
|
-
h('span', { class: 'ds-ep-propfield-value' }, ...kids(children)),
|
|
209
|
-
hint != null ? h('span', { class: 'ds-ep-propfield-hint' }, hint) : null
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
// PropertyGridRow — a PropertyGrid row wrapper with a bottom-border divider
|
|
215
|
-
// (last-child border suppressed), for editors that need a stronger per-row
|
|
216
|
-
// visual separation than the default PropertyGrid gap gives (e.g. a list of
|
|
217
|
-
// independently-editable records like PRD/mutable rows). Generalizes
|
|
218
|
-
// gmsniff's gm-propgrid-row.
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
export function PropertyGridRow({ children, key } = {}) {
|
|
221
|
-
return h('div', { key, class: 'ds-ep-propgrid-row' }, ...kids(children));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
// InlineEditableField — a borderless-until-focus text input that inherits
|
|
226
|
-
// surrounding font (no boxed input chrome), with an explicit error state
|
|
227
|
-
// (aria-invalid + danger-token border) for live per-field validation.
|
|
228
|
-
// Generalizes gmsniff's gm-inline-input / gm-field-error pair. Renders a
|
|
229
|
-
// <textarea> when multiline is set (for longer free-text edits), else a
|
|
230
|
-
// single-line <input>.
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
export function InlineEditableField({ value = '', placeholder, onInput, onChange, error, multiline = false, rows = 3, ariaLabel, disabled = false } = {}) {
|
|
233
|
-
const cls = 'ds-ep-inline-input' + (error ? ' has-error' : '');
|
|
234
|
-
const common = {
|
|
235
|
-
class: cls,
|
|
236
|
-
value,
|
|
237
|
-
placeholder,
|
|
238
|
-
disabled: disabled ? 'disabled' : null,
|
|
239
|
-
'aria-label': ariaLabel,
|
|
240
|
-
'aria-invalid': error ? 'true' : null,
|
|
241
|
-
oninput: onInput ? (e) => onInput(e.target.value, e) : null,
|
|
242
|
-
onchange: onChange ? (e) => onChange(e.target.value, e) : null,
|
|
243
|
-
};
|
|
244
|
-
return multiline
|
|
245
|
-
? h('textarea', { ...common, rows })
|
|
246
|
-
: h('input', { ...common, type: 'text' });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function Dock({ top, left, right, bottom, center } = {}) {
|
|
250
|
-
return h('div', { class: 'ds-ep-dock' },
|
|
251
|
-
top != null ? h('div', { class: 'ds-ep-dock-top' }, ...kids(top)) : null,
|
|
252
|
-
left != null ? h('div', { class: 'ds-ep-dock-left' }, ...kids(left)) : null,
|
|
253
|
-
h('div', { class: 'ds-ep-dock-center' }, ...kids(center)),
|
|
254
|
-
right != null ? h('div', { class: 'ds-ep-dock-right' }, ...kids(right)) : null,
|
|
255
|
-
bottom != null ? h('div', { class: 'ds-ep-dock-bottom' }, ...kids(bottom)) : null
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// Breakpoints + useMediaQuery
|
|
261
|
-
// ---------------------------------------------------------------------------
|
|
262
|
-
export const BP_SM = 480;
|
|
263
|
-
export const BP_MD = 768;
|
|
264
|
-
export const BP_LG = 1024;
|
|
265
|
-
export const BP_XL = 1440;
|
|
266
|
-
|
|
267
|
-
export function useMediaQuery(query) {
|
|
268
|
-
if (typeof window === 'undefined' || !window.matchMedia) {
|
|
269
|
-
return { matches: false, addListener: () => {}, removeListener: () => {} };
|
|
270
|
-
}
|
|
271
|
-
const mql = window.matchMedia(query);
|
|
272
|
-
return {
|
|
273
|
-
get matches() { return mql.matches; },
|
|
274
|
-
addListener(fn) { mql.addEventListener ? mql.addEventListener('change', fn) : mql.addListener(fn); },
|
|
275
|
-
removeListener(fn) { mql.removeEventListener ? mql.removeEventListener('change', fn) : mql.removeListener(fn); },
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// ---------------------------------------------------------------------------
|
|
280
|
-
// Grid / GridItem — 24-column responsive layout primitive (screen-real-estate
|
|
281
|
-
// density: dense multi-column panels without a hand-rolled grid-template-
|
|
282
|
-
// columns per consumer). Column-span props are integers 1-24 (or `true` for
|
|
283
|
-
// full-width/auto-grow, or `0` to hide at that breakpoint) evaluated at four
|
|
284
|
-
// tiers mirroring BP_SM/MD/LG/XL (480/768/1024/1440) via media queries in
|
|
285
|
-
// editor-primitives.css — no JS-side matchMedia needed, CSS custom
|
|
286
|
-
// properties + @media do the layout work so it degrades gracefully with
|
|
287
|
-
// SSR/no-hydration. Grid itself is a flex row wrapper; GridItem computes
|
|
288
|
-
// flex-basis/max-width from its span at each tier.
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
export function Grid({ gap, justify, align, children, key } = {}) {
|
|
291
|
-
const style = [
|
|
292
|
-
gap != null ? `gap:${typeof gap === 'number' ? gap + 'px' : gap}` : '',
|
|
293
|
-
justify ? `justify-content:${justify}` : '',
|
|
294
|
-
align ? `align-items:${align}` : '',
|
|
295
|
-
].filter(Boolean).join(';');
|
|
296
|
-
return h('div', { key, class: 'ds-ep-grid', style: style || null }, children);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function gridSpanStyle(prefix, val) {
|
|
300
|
-
if (val === undefined) return '';
|
|
301
|
-
if (val === true) return `--${prefix}-basis:100%;--${prefix}-grow:1;--${prefix}-display:inherit;`;
|
|
302
|
-
if (val === 0) return `--${prefix}-display:none;`;
|
|
303
|
-
const pct = Math.max(0, Math.min(100, (100 / 24) * val));
|
|
304
|
-
return `--${prefix}-basis:${pct}%;--${prefix}-grow:0;--${prefix}-display:inherit;`;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function GridItem({ xs, sm, md, lg, xl, children, key } = {}) {
|
|
308
|
-
const style = [
|
|
309
|
-
gridSpanStyle('xs', xs),
|
|
310
|
-
gridSpanStyle('sm', sm),
|
|
311
|
-
gridSpanStyle('md', md),
|
|
312
|
-
gridSpanStyle('lg', lg),
|
|
313
|
-
gridSpanStyle('xl', xl),
|
|
314
|
-
].join('');
|
|
315
|
-
return h('div', { key, class: 'ds-ep-grid-item', style: style || null }, children);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// FocusTrap — wraps subtree, traps Tab/Shift+Tab. Mount/unmount lifecycle is
|
|
320
|
-
// managed via DOM-level keydown listener attached when first focused.
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
const FOCUSABLE_SEL = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
323
|
-
|
|
324
|
-
function trapTabKey(rootEl, e) {
|
|
325
|
-
if (e.key !== 'Tab') return;
|
|
326
|
-
const nodes = rootEl.querySelectorAll(FOCUSABLE_SEL);
|
|
327
|
-
if (!nodes.length) { e.preventDefault(); return; }
|
|
328
|
-
const first = nodes[0], last = nodes[nodes.length - 1];
|
|
329
|
-
const active = (rootEl.getRootNode && rootEl.getRootNode().activeElement) || document.activeElement;
|
|
330
|
-
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
|
|
331
|
-
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function FocusTrap({ children } = {}) {
|
|
335
|
-
return h('div', {
|
|
336
|
-
class: 'ds-ep-focustrap',
|
|
337
|
-
tabindex: '-1',
|
|
338
|
-
ref: (el) => {
|
|
339
|
-
if (!el || el._dsTrap) return;
|
|
340
|
-
el._dsTrap = true;
|
|
341
|
-
el.addEventListener('keydown', (e) => trapTabKey(el, e));
|
|
342
|
-
// Auto-focus first focusable
|
|
343
|
-
queueMicrotask(() => {
|
|
344
|
-
const first = el.querySelector(FOCUSABLE_SEL);
|
|
345
|
-
if (first) first.focus();
|
|
346
|
-
else el.focus();
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}, ...kids(children));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ---------------------------------------------------------------------------
|
|
353
|
-
// ResizeHandle — splitter, axis = 'horizontal' (vertical bar, horiz drag)
|
|
354
|
-
// or 'vertical' (horizontal bar, vertical drag). onResize(delta:px).
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
export function ResizeHandle({ axis = 'horizontal', onResize, ariaLabel } = {}) {
|
|
357
|
-
const isH = axis === 'horizontal';
|
|
358
|
-
let dragOrigin = null;
|
|
359
|
-
const step = 8;
|
|
360
|
-
const emit = (dx, dy) => { if (onResize) onResize(isH ? dx : dy); };
|
|
361
|
-
const onPointerDown = (e) => {
|
|
362
|
-
e.preventDefault();
|
|
363
|
-
dragOrigin = { x: e.clientX, y: e.clientY };
|
|
364
|
-
e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId);
|
|
365
|
-
};
|
|
366
|
-
const onPointerMove = (e) => {
|
|
367
|
-
if (!dragOrigin) return;
|
|
368
|
-
const dx = e.clientX - dragOrigin.x;
|
|
369
|
-
const dy = e.clientY - dragOrigin.y;
|
|
370
|
-
dragOrigin = { x: e.clientX, y: e.clientY };
|
|
371
|
-
emit(dx, dy);
|
|
372
|
-
};
|
|
373
|
-
const onPointerUp = (e) => {
|
|
374
|
-
dragOrigin = null;
|
|
375
|
-
try { e.currentTarget.releasePointerCapture && e.currentTarget.releasePointerCapture(e.pointerId); } catch { /* swallow: pointer capture may already be released, drag end still proceeds */ }
|
|
376
|
-
};
|
|
377
|
-
const onKeyDown = (e) => {
|
|
378
|
-
const k = e.key;
|
|
379
|
-
if (isH) {
|
|
380
|
-
if (k === 'ArrowLeft') { e.preventDefault(); emit(-step, 0); }
|
|
381
|
-
else if (k === 'ArrowRight') { e.preventDefault(); emit(step, 0); }
|
|
382
|
-
else if (k === 'Home') { e.preventDefault(); emit(-1e6, 0); }
|
|
383
|
-
else if (k === 'End') { e.preventDefault(); emit(1e6, 0); }
|
|
384
|
-
} else {
|
|
385
|
-
if (k === 'ArrowUp') { e.preventDefault(); emit(0, -step); }
|
|
386
|
-
else if (k === 'ArrowDown') { e.preventDefault(); emit(0, step); }
|
|
387
|
-
else if (k === 'Home') { e.preventDefault(); emit(0, -1e6); }
|
|
388
|
-
else if (k === 'End') { e.preventDefault(); emit(0, 1e6); }
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
return h('div', {
|
|
392
|
-
class: 'ds-ep-resize ' + (isH ? 'axis-h' : 'axis-v'),
|
|
393
|
-
role: 'separator',
|
|
394
|
-
tabindex: '0',
|
|
395
|
-
'aria-orientation': isH ? 'vertical' : 'horizontal',
|
|
396
|
-
'aria-label': ariaLabel || 'Resize',
|
|
397
|
-
onpointerdown: onPointerDown,
|
|
398
|
-
onpointermove: onPointerMove,
|
|
399
|
-
onpointerup: onPointerUp,
|
|
400
|
-
onpointercancel: onPointerUp,
|
|
401
|
-
onkeydown: onKeyDown,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ---------------------------------------------------------------------------
|
|
406
|
-
// SplitPanel — two children separated by a ResizeHandle. Stateful via DOM.
|
|
407
|
-
// ---------------------------------------------------------------------------
|
|
408
|
-
export function SplitPanel({ orientation = 'horizontal', initial = '50%', min = 80, max = Infinity, children } = {}) {
|
|
409
|
-
const isH = orientation === 'horizontal';
|
|
410
|
-
const ks = kids(children);
|
|
411
|
-
const first = ks[0] || null;
|
|
412
|
-
const second = ks[1] || null;
|
|
413
|
-
const sizeProp = isH ? 'width' : 'height';
|
|
414
|
-
const initStyle = typeof initial === 'number' ? initial + 'px' : initial;
|
|
415
|
-
let rootEl = null;
|
|
416
|
-
// The dragged size is persisted here so a re-render (applyDiff reconciling
|
|
417
|
-
// the pane's style back to the initial value) does NOT reset the user's
|
|
418
|
-
// resize. onResize records it; the pane's ref re-applies it after each diff.
|
|
419
|
-
let draggedSize = null;
|
|
420
|
-
const applySize = (a) => {
|
|
421
|
-
if (!a) return;
|
|
422
|
-
if (draggedSize != null) { a.style[sizeProp] = draggedSize + 'px'; a.style.flex = '0 0 auto'; }
|
|
423
|
-
};
|
|
424
|
-
const onResize = (delta) => {
|
|
425
|
-
if (!rootEl) return;
|
|
426
|
-
const a = rootEl.firstChild;
|
|
427
|
-
if (!a) return;
|
|
428
|
-
const rect = a.getBoundingClientRect();
|
|
429
|
-
const curr = isH ? rect.width : rect.height;
|
|
430
|
-
const total = isH ? rootEl.getBoundingClientRect().width : rootEl.getBoundingClientRect().height;
|
|
431
|
-
const next = Math.max(min, Math.min(max === Infinity ? total - min : max, curr + delta));
|
|
432
|
-
draggedSize = next;
|
|
433
|
-
a.style[sizeProp] = next + 'px';
|
|
434
|
-
a.style.flex = '0 0 auto';
|
|
435
|
-
};
|
|
436
|
-
return h('div', {
|
|
437
|
-
class: 'ds-ep-split ' + (isH ? 'horiz' : 'vert'),
|
|
438
|
-
ref: (el) => { rootEl = el; }
|
|
439
|
-
},
|
|
440
|
-
h('div', { class: 'ds-ep-split-pane', style: '--split-size:' + initStyle + ';flex:0 0 auto', ref: applySize }, first),
|
|
441
|
-
ResizeHandle({ axis: isH ? 'horizontal' : 'vertical', onResize }),
|
|
442
|
-
h('div', { class: 'ds-ep-split-pane grow', style: 'flex:1 1 0;min-' + sizeProp + ':0' }, second)
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
// ContextMenu — items, anchor {x,y}, onClose. Viewport-clamped. Keyboard nav.
|
|
448
|
-
// ---------------------------------------------------------------------------
|
|
449
|
-
export function ContextMenu({ items = [], anchor = { x: 0, y: 0 }, onClose } = {}) {
|
|
450
|
-
let rootEl = null;
|
|
451
|
-
const close = () => { if (onClose) onClose(); };
|
|
452
|
-
const select = (it) => {
|
|
453
|
-
if (it.disabled || it.separator) return;
|
|
454
|
-
if (it.onSelect) it.onSelect();
|
|
455
|
-
close();
|
|
456
|
-
};
|
|
457
|
-
const onKey = (e) => {
|
|
458
|
-
const btns = rootEl ? [...rootEl.querySelectorAll('button[data-ix]')] : [];
|
|
459
|
-
const active = document.activeElement;
|
|
460
|
-
const idx = btns.indexOf(active);
|
|
461
|
-
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
462
|
-
else if (e.key === 'ArrowDown') { e.preventDefault(); (btns[(idx + 1) % btns.length] || btns[0])?.focus(); }
|
|
463
|
-
else if (e.key === 'ArrowUp') { e.preventDefault(); (btns[(idx - 1 + btns.length) % btns.length] || btns[0])?.focus(); }
|
|
464
|
-
else if (e.key === 'Enter' && idx >= 0) { e.preventDefault(); btns[idx].click(); }
|
|
465
|
-
};
|
|
466
|
-
return h('div', {
|
|
467
|
-
class: 'ds-ep-ctxmenu-backdrop',
|
|
468
|
-
onmousedown: (e) => { if (e.target === e.currentTarget) close(); },
|
|
469
|
-
oncontextmenu: (e) => { e.preventDefault(); close(); },
|
|
470
|
-
},
|
|
471
|
-
h('div', {
|
|
472
|
-
class: 'ds-ep-ctxmenu',
|
|
473
|
-
role: 'menu',
|
|
474
|
-
tabindex: '-1',
|
|
475
|
-
onkeydown: onKey,
|
|
476
|
-
ref: (el) => {
|
|
477
|
-
if (!el) {
|
|
478
|
-
// Unmount: unhook the resize re-clamp bound on mount.
|
|
479
|
-
if (rootEl && rootEl._dsCtxClampOff) { rootEl._dsCtxClampOff(); }
|
|
480
|
-
rootEl = null;
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
rootEl = el;
|
|
484
|
-
// Position at the anchor immediately, then clamp once layout has
|
|
485
|
-
// settled — measuring synchronously in ref reads a zero-size box
|
|
486
|
-
// (children not yet painted), so the clamp must run post-layout.
|
|
487
|
-
const ax = anchor.x || 0, ay = anchor.y || 0;
|
|
488
|
-
el.style.left = ax + 'px';
|
|
489
|
-
el.style.top = ay + 'px';
|
|
490
|
-
const coarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
|
|
491
|
-
const clamp = () => {
|
|
492
|
-
const vw = window.innerWidth, vh = window.innerHeight;
|
|
493
|
-
const r = el.getBoundingClientRect();
|
|
494
|
-
let x = ax, y = ay;
|
|
495
|
-
// Touch: keep the menu clear of the lifting finger — nudge
|
|
496
|
-
// below the touch point, or open above when it fits and the
|
|
497
|
-
// anchor sits in the lower half (lift-off would otherwise
|
|
498
|
-
// activate the first item).
|
|
499
|
-
if (coarse) {
|
|
500
|
-
y = ay + 10;
|
|
501
|
-
if (ay > vh / 2 && ay - r.height >= 4) y = ay - r.height;
|
|
502
|
-
}
|
|
503
|
-
if (x + r.width > vw) x = Math.max(4, vw - r.width - 4);
|
|
504
|
-
if (y + r.height > vh) y = Math.max(4, vh - r.height - 4);
|
|
505
|
-
el.style.left = x + 'px';
|
|
506
|
-
el.style.top = y + 'px';
|
|
507
|
-
};
|
|
508
|
-
requestAnimationFrame(clamp);
|
|
509
|
-
// Re-clamp on resize/orientation change for the menu's lifetime.
|
|
510
|
-
window.addEventListener('resize', clamp);
|
|
511
|
-
el._dsCtxClampOff = () => { window.removeEventListener('resize', clamp); el._dsCtxClampOff = null; };
|
|
512
|
-
// setTimeout(0), not queueMicrotask: the triggering contextmenu/click
|
|
513
|
-
// event's own default focus can otherwise win the race and leave focus
|
|
514
|
-
// outside the menu, breaking keyboard arrow-nav/Escape.
|
|
515
|
-
setTimeout(() => { el.querySelector('button[data-ix]')?.focus(); }, 0);
|
|
516
|
-
}
|
|
517
|
-
},
|
|
518
|
-
...items.map((it, i) => it.separator
|
|
519
|
-
? h('div', { key: 'sep' + i, class: 'ds-ep-ctxmenu-sep', role: 'separator' })
|
|
520
|
-
: h('button', {
|
|
521
|
-
key: i, type: 'button', role: 'menuitem',
|
|
522
|
-
'data-ix': String(i),
|
|
523
|
-
class: 'ds-ep-ctxmenu-item' + (it.danger ? ' danger' : '') + (it.disabled ? ' disabled' : ''),
|
|
524
|
-
disabled: it.disabled ? 'disabled' : null,
|
|
525
|
-
onclick: () => select(it),
|
|
526
|
-
},
|
|
527
|
-
it.icon != null ? h('span', { class: 'ds-ep-ctxmenu-icon' }, it.icon) : null,
|
|
528
|
-
h('span', { class: 'ds-ep-ctxmenu-label' }, it.label)
|
|
529
|
-
))
|
|
530
|
-
)
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Helper: wires right-click + long-press to a target ref. Caller manages state.
|
|
535
|
-
export function useContextMenu(targetEl, items, openCb) {
|
|
536
|
-
if (!targetEl) return () => {};
|
|
537
|
-
let touchTimer = null, lastOpen = 0;
|
|
538
|
-
// Android fires the native contextmenu event on long-press AND our 500ms
|
|
539
|
-
// touch timer — dedupe so the menu opens once, not twice (open/flicker).
|
|
540
|
-
const open = (x, y) => {
|
|
541
|
-
if (Date.now() - lastOpen < 700) return;
|
|
542
|
-
lastOpen = Date.now();
|
|
543
|
-
if (openCb) openCb({ x, y, items });
|
|
544
|
-
};
|
|
545
|
-
const onCtx = (e) => { e.preventDefault(); open(e.clientX, e.clientY); };
|
|
546
|
-
const onTouchStart = (e) => {
|
|
547
|
-
const t = e.touches && e.touches[0]; if (!t) return;
|
|
548
|
-
touchTimer = setTimeout(() => { touchTimer = null; open(t.clientX, t.clientY); }, 500);
|
|
549
|
-
};
|
|
550
|
-
const cancel = () => { if (touchTimer) { clearTimeout(touchTimer); touchTimer = null; } };
|
|
551
|
-
targetEl.addEventListener('contextmenu', onCtx);
|
|
552
|
-
targetEl.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
553
|
-
targetEl.addEventListener('touchmove', cancel, { passive: true });
|
|
554
|
-
targetEl.addEventListener('touchend', cancel);
|
|
555
|
-
targetEl.addEventListener('touchcancel', cancel);
|
|
556
|
-
return () => {
|
|
557
|
-
targetEl.removeEventListener('contextmenu', onCtx);
|
|
558
|
-
targetEl.removeEventListener('touchstart', onTouchStart);
|
|
559
|
-
targetEl.removeEventListener('touchmove', cancel);
|
|
560
|
-
targetEl.removeEventListener('touchend', cancel);
|
|
561
|
-
targetEl.removeEventListener('touchcancel', cancel);
|
|
562
|
-
cancel();
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// ---------------------------------------------------------------------------
|
|
567
|
-
// Drawer — slide-in from side. side='left'|'right'|'bottom'.
|
|
568
|
-
// ---------------------------------------------------------------------------
|
|
569
|
-
export function Drawer({ side = 'left', open = false, onClose, children, ariaLabel } = {}) {
|
|
570
|
-
if (!open) return null;
|
|
571
|
-
const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose && onClose(); } };
|
|
572
|
-
return h('div', {
|
|
573
|
-
class: 'ds-ep-drawer-backdrop',
|
|
574
|
-
onmousedown: (e) => { if (e.target === e.currentTarget) onClose && onClose(); },
|
|
575
|
-
},
|
|
576
|
-
h('div', {
|
|
577
|
-
class: 'ds-ep-drawer side-' + side,
|
|
578
|
-
role: 'dialog',
|
|
579
|
-
'aria-modal': 'true',
|
|
580
|
-
'aria-label': ariaLabel || 'Drawer',
|
|
581
|
-
tabindex: '-1',
|
|
582
|
-
onkeydown: onKey,
|
|
583
|
-
ref: (el) => {
|
|
584
|
-
if (!el || el._dsTrap) return;
|
|
585
|
-
el._dsTrap = true;
|
|
586
|
-
el.addEventListener('keydown', (e) => trapTabKey(el, e));
|
|
587
|
-
// setTimeout(0), not queueMicrotask — see Dialog's identical comment
|
|
588
|
-
// below: the opening click's own default focus can win a same-tick race.
|
|
589
|
-
setTimeout(() => {
|
|
590
|
-
const f = el.querySelector(FOCUSABLE_SEL);
|
|
591
|
-
(f || el).focus();
|
|
592
|
-
}, 0);
|
|
593
|
-
},
|
|
594
|
-
}, ...kids(children))
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// ---------------------------------------------------------------------------
|
|
599
|
-
// Dialog — modal. actions = [{label, onClick, kind?}], dismissible (backdrop).
|
|
600
|
-
// ---------------------------------------------------------------------------
|
|
601
|
-
export function Dialog({ title, open = false, onClose, children, actions = [], dismissible = false, ariaLabel } = {}) {
|
|
602
|
-
if (!open) return null;
|
|
603
|
-
const opener = (typeof document !== 'undefined') ? document.activeElement : null;
|
|
604
|
-
const close = () => {
|
|
605
|
-
if (onClose) onClose();
|
|
606
|
-
if (opener && opener.focus) queueMicrotask(() => opener.focus());
|
|
607
|
-
};
|
|
608
|
-
const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } };
|
|
609
|
-
return h('div', {
|
|
610
|
-
class: 'ds-ep-dialog-backdrop',
|
|
611
|
-
onmousedown: (e) => { if (dismissible && e.target === e.currentTarget) close(); },
|
|
612
|
-
},
|
|
613
|
-
h('div', {
|
|
614
|
-
class: 'ds-ep-dialog',
|
|
615
|
-
role: 'dialog',
|
|
616
|
-
'aria-modal': 'true',
|
|
617
|
-
'aria-label': ariaLabel || title || 'Dialog',
|
|
618
|
-
tabindex: '-1',
|
|
619
|
-
onkeydown: onKey,
|
|
620
|
-
ref: (el) => {
|
|
621
|
-
if (!el || el._dsTrap) return;
|
|
622
|
-
el._dsTrap = true;
|
|
623
|
-
el.addEventListener('keydown', (e) => trapTabKey(el, e));
|
|
624
|
-
// setTimeout(0), not queueMicrotask: the triggering click's own default
|
|
625
|
-
// focus-on-click can win a same-tick microtask race and leave focus on
|
|
626
|
-
// the trigger button instead of the dialog, breaking Escape/Tab-trap
|
|
627
|
-
// for keyboard users (keydown only bubbles from the focused element).
|
|
628
|
-
setTimeout(() => {
|
|
629
|
-
const f = el.querySelector(FOCUSABLE_SEL);
|
|
630
|
-
(f || el).focus();
|
|
631
|
-
}, 0);
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
title != null ? h('div', { class: 'ds-ep-dialog-head' }, h('h2', { class: 'ds-ep-dialog-title' }, title)) : null,
|
|
635
|
-
h('div', { class: 'ds-ep-dialog-body' }, ...kids(children)),
|
|
636
|
-
actions && actions.length ? h('div', { class: 'ds-ep-dialog-actions' },
|
|
637
|
-
...actions.map((a, i) => h('button', {
|
|
638
|
-
key: i, type: 'button',
|
|
639
|
-
class: 'ds-ep-dialog-btn' + (a.kind ? (' kind-' + a.kind) : ''),
|
|
640
|
-
onclick: (e) => { if (a.onClick) a.onClick(e); if (a.close !== false) close(); }
|
|
641
|
-
}, a.label))
|
|
642
|
-
) : null
|
|
643
|
-
)
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ---------------------------------------------------------------------------
|
|
648
|
-
// Toast — Toast({message,kind,duration}) component + imperative toast(opts).
|
|
649
|
-
// ---------------------------------------------------------------------------
|
|
650
|
-
export function Toast({ message, kind = 'info', duration = 3000, onClose } = {}) {
|
|
651
|
-
return h('div', {
|
|
652
|
-
class: 'ds-ep-toast kind-' + kind,
|
|
653
|
-
role: 'status',
|
|
654
|
-
'aria-live': 'polite',
|
|
655
|
-
ref: (el) => {
|
|
656
|
-
if (!el || el._dsToast) return;
|
|
657
|
-
el._dsToast = true;
|
|
658
|
-
if (duration > 0) setTimeout(() => { onClose && onClose(); el.classList.add('leaving'); }, duration);
|
|
659
|
-
}
|
|
660
|
-
}, message);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
let _toastHostEl = null;
|
|
664
|
-
function ensureToastHost() {
|
|
665
|
-
if (typeof document === 'undefined') return null;
|
|
666
|
-
if (_toastHostEl && document.body.contains(_toastHostEl)) return _toastHostEl;
|
|
667
|
-
_toastHostEl = document.createElement('div');
|
|
668
|
-
_toastHostEl.className = 'ds-ep-toast-host';
|
|
669
|
-
document.body.appendChild(_toastHostEl);
|
|
670
|
-
return _toastHostEl;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
export function toast({ message, kind = 'info', duration = 3000, actionLabel, onAction } = {}) {
|
|
674
|
-
const host = ensureToastHost();
|
|
675
|
-
if (!host) return () => {};
|
|
676
|
-
const el = document.createElement('div');
|
|
677
|
-
el.className = 'ds-ep-toast kind-' + kind;
|
|
678
|
-
el.setAttribute('role', 'status');
|
|
679
|
-
el.setAttribute('aria-live', 'polite');
|
|
680
|
-
const text = document.createElement('span');
|
|
681
|
-
text.className = 'ds-ep-toast-msg';
|
|
682
|
-
text.textContent = message;
|
|
683
|
-
el.appendChild(text);
|
|
684
|
-
const dismiss = () => {
|
|
685
|
-
if (!el.parentNode) return;
|
|
686
|
-
el.classList.add('leaving');
|
|
687
|
-
setTimeout(() => { el.parentNode && el.parentNode.removeChild(el); }, 200);
|
|
688
|
-
};
|
|
689
|
-
if (actionLabel && onAction) {
|
|
690
|
-
el.classList.add('has-action');
|
|
691
|
-
const btn = document.createElement('button');
|
|
692
|
-
btn.type = 'button';
|
|
693
|
-
btn.className = 'ds-ep-toast-action';
|
|
694
|
-
btn.textContent = actionLabel;
|
|
695
|
-
btn.onclick = () => onAction(dismiss);
|
|
696
|
-
el.appendChild(btn);
|
|
697
|
-
}
|
|
698
|
-
host.appendChild(el);
|
|
699
|
-
if (duration > 0) setTimeout(dismiss, duration);
|
|
700
|
-
return dismiss;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// ---------------------------------------------------------------------------
|
|
704
|
-
// Pager — prev/next paginator with a page label. Generalizes gmsniff's
|
|
705
|
-
// gm-pager. page is 1-indexed; pageCount<=1 disables both buttons (no
|
|
706
|
-
// divide-by-zero, no dead-end enabled control). total (optional) renders an
|
|
707
|
-
// item-count suffix ("42 items") alongside the page label.
|
|
708
6
|
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const nextBtn = h('button', {
|
|
745
|
-
type: 'button', class: 'ds-ep-pager-btn', disabled: atEnd ? 'disabled' : null,
|
|
746
|
-
'aria-label': 'next page',
|
|
747
|
-
onclick: () => { if (!atEnd && onPage) onPage(safePage + 1); },
|
|
748
|
-
}, '->');
|
|
749
|
-
if (numbered) {
|
|
750
|
-
const range = buildPageRange(safeCount, safePage, Math.max(1, siblingCount || 1));
|
|
751
|
-
return h('div', { class: 'ds-ep-pager ds-ep-pager-numbered', role: 'group', 'aria-label': 'pagination' },
|
|
752
|
-
prevBtn,
|
|
753
|
-
...range.map((p, i) => p === '...'
|
|
754
|
-
? h('span', { key: 'ellipsis-' + i, class: 'ds-ep-pager-ellipsis' }, '...')
|
|
755
|
-
: h('button', {
|
|
756
|
-
key: 'p' + p, type: 'button',
|
|
757
|
-
class: 'ds-ep-pager-num' + (p === safePage ? ' is-current' : ''),
|
|
758
|
-
'aria-current': p === safePage ? 'page' : null,
|
|
759
|
-
'aria-label': 'page ' + p,
|
|
760
|
-
onclick: () => { if (p !== safePage && onPage) onPage(p); },
|
|
761
|
-
}, String(p))),
|
|
762
|
-
nextBtn,
|
|
763
|
-
total != null ? h('span', { class: 'ds-ep-pager-total' }, total + ' ' + itemLabel) : null
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
return h('div', { class: 'ds-ep-pager', role: 'group', 'aria-label': 'pagination' },
|
|
767
|
-
prevBtn,
|
|
768
|
-
h('span', { class: 'ds-ep-pager-label' },
|
|
769
|
-
'page ' + safePage + ' / ' + safeCount + (total != null ? ' (' + total + ' ' + itemLabel + ')' : '')),
|
|
770
|
-
nextBtn
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// ---------------------------------------------------------------------------
|
|
775
|
-
// JsonViewer — monospace data preview (max-height + scroll), generalizing
|
|
776
|
-
// gmsniff's gm-json. Accepts a pre-stringified string OR any value
|
|
777
|
-
// (objects/arrays get JSON.stringify(v, null, 2); null/undefined render the
|
|
778
|
-
// empty-state text rather than the literal string "undefined"/"null").
|
|
779
|
-
//
|
|
780
|
-
// mode selects rendering; 'plain' is the historical contract (children[0] is
|
|
781
|
-
// the raw text string, verbatim for string input) and stays the default so
|
|
782
|
-
// every existing consumer is untouched:
|
|
783
|
-
// 'plain' — flat <pre>, raw text.
|
|
784
|
-
// 'highlight' — flat <pre>, text tokenized into ds-ep-json-* spans
|
|
785
|
-
// (key/string/number/boolean/null). A string that does not
|
|
786
|
-
// parse as JSON falls back to plain text — arbitrary prose is
|
|
787
|
-
// never falsely tokenized.
|
|
788
|
-
// 'tree' — collapsible <details> tree per nested object/array, open
|
|
789
|
-
// above treeDepth (default 2), each summary carrying a
|
|
790
|
-
// child-count tag. Scalars/unparseable input fall back to
|
|
791
|
-
// 'highlight'/plain respectively.
|
|
792
|
-
// copyable=true wraps the viewer with a copy-to-clipboard button (transient
|
|
793
|
-
// copied/failed feedback, no dependencies).
|
|
794
|
-
// ---------------------------------------------------------------------------
|
|
795
|
-
const JSON_NUM_CHARS = '0123456789eE+.-';
|
|
796
|
-
|
|
797
|
-
// Linear single-pass scan — no regex, no backtracking, safe on truncated
|
|
798
|
-
// input (an unterminated string just consumes to end-of-text).
|
|
799
|
-
function tokenizeJson(text) {
|
|
800
|
-
const toks = [];
|
|
801
|
-
let i = 0, plain = '';
|
|
802
|
-
const flush = () => { if (plain) { toks.push(['', plain]); plain = ''; } };
|
|
803
|
-
while (i < text.length) {
|
|
804
|
-
const c = text[i];
|
|
805
|
-
if (c === '"') {
|
|
806
|
-
const start = i;
|
|
807
|
-
i++;
|
|
808
|
-
while (i < text.length && text[i] !== '"') { if (text[i] === '\\') i++; i++; }
|
|
809
|
-
i = Math.min(i + 1, text.length);
|
|
810
|
-
let j = i;
|
|
811
|
-
while (j < text.length && (text[j] === ' ' || text[j] === '\t' || text[j] === '\n' || text[j] === '\r')) j++;
|
|
812
|
-
flush();
|
|
813
|
-
toks.push([text[j] === ':' ? 'k' : 's', text.slice(start, i)]);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
if (c === '-' || (c >= '0' && c <= '9')) {
|
|
817
|
-
const start = i;
|
|
818
|
-
i++;
|
|
819
|
-
while (i < text.length && JSON_NUM_CHARS.includes(text[i])) i++;
|
|
820
|
-
flush();
|
|
821
|
-
toks.push(['n', text.slice(start, i)]);
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
if (text.startsWith('true', i)) { flush(); toks.push(['b', 'true']); i += 4; continue; }
|
|
825
|
-
if (text.startsWith('false', i)) { flush(); toks.push(['b', 'false']); i += 5; continue; }
|
|
826
|
-
if (text.startsWith('null', i)) { flush(); toks.push(['z', 'null']); i += 4; continue; }
|
|
827
|
-
plain += c; i++;
|
|
828
|
-
}
|
|
829
|
-
flush();
|
|
830
|
-
return toks;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function highlightJsonSpans(text) {
|
|
834
|
-
return tokenizeJson(text).map(([t, s]) => t ? h('span', { class: 'ds-ep-json-' + t }, s) : s);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function jsonTreeNode(key, val, depth, treeDepth) {
|
|
838
|
-
const keyParts = key != null ? [h('span', { class: 'ds-ep-json-k' }, JSON.stringify(key)), ': '] : [];
|
|
839
|
-
if (val !== null && typeof val === 'object') {
|
|
840
|
-
const isArr = Array.isArray(val);
|
|
841
|
-
const entries = isArr ? val.map((v) => [null, v]) : Object.entries(val);
|
|
842
|
-
if (!entries.length) return h('div', { class: 'ds-ep-json-leaf' }, ...keyParts, isArr ? '[]' : '{}');
|
|
843
|
-
const tag = isArr ? '[' + entries.length + ']' : '{' + entries.length + '}';
|
|
844
|
-
return h('details', { class: 'ds-ep-json-node', open: depth < treeDepth ? true : null },
|
|
845
|
-
h('summary', { class: 'ds-ep-json-sum' }, ...keyParts, h('span', { class: 'ds-ep-json-tag' }, tag)),
|
|
846
|
-
h('div', { class: 'ds-ep-json-kids' }, ...entries.map(([k, v]) => jsonTreeNode(k, v, depth + 1, treeDepth))));
|
|
847
|
-
}
|
|
848
|
-
const t = typeof val === 'string' ? 's' : typeof val === 'number' ? 'n' : typeof val === 'boolean' ? 'b' : 'z';
|
|
849
|
-
return h('div', { class: 'ds-ep-json-leaf' }, ...keyParts, h('span', { class: 'ds-ep-json-' + t }, JSON.stringify(val) ?? String(val)));
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function jsonCopyButton(text) {
|
|
853
|
-
return h('button', {
|
|
854
|
-
type: 'button', class: 'ds-ep-json-copy', title: 'copy JSON', 'aria-label': 'copy JSON',
|
|
855
|
-
onclick: (e) => {
|
|
856
|
-
const btn = e.currentTarget;
|
|
857
|
-
const show = (label, ok) => {
|
|
858
|
-
btn.textContent = label;
|
|
859
|
-
btn.classList.toggle('copied', ok);
|
|
860
|
-
setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('copied'); }, 1200);
|
|
861
|
-
};
|
|
862
|
-
try {
|
|
863
|
-
navigator.clipboard.writeText(text).then(() => show('copied', true), () => show('failed', false));
|
|
864
|
-
} catch {
|
|
865
|
-
show('failed', false);
|
|
866
|
-
}
|
|
867
|
-
},
|
|
868
|
-
}, 'copy');
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
export function JsonViewer({ value, emptyText = 'no data', maxHeight, mode = 'plain', copyable = false, treeDepth = 2 } = {}) {
|
|
872
|
-
let text, parsed;
|
|
873
|
-
let knownJson = false;
|
|
874
|
-
if (value == null) text = null;
|
|
875
|
-
else if (typeof value === 'string') text = value;
|
|
876
|
-
else { try { text = JSON.stringify(value, null, 2); knownJson = text != null; parsed = value; } catch { text = String(value); } }
|
|
877
|
-
if (!text) return h('div', { class: 'ds-ep-json ds-ep-json-empty' }, emptyText);
|
|
878
|
-
const style = maxHeight ? ('max-height:' + maxHeight) : null;
|
|
879
|
-
if (!knownJson && (mode === 'highlight' || mode === 'tree')) {
|
|
880
|
-
try { parsed = JSON.parse(text); knownJson = true; } catch { /* swallow: not JSON — render plain */ }
|
|
881
|
-
}
|
|
882
|
-
let body;
|
|
883
|
-
if (mode === 'tree' && knownJson && parsed !== null && typeof parsed === 'object') {
|
|
884
|
-
body = h('div', { class: 'ds-ep-json ds-ep-json-tree', style }, jsonTreeNode(null, parsed, 0, treeDepth));
|
|
885
|
-
} else if ((mode === 'highlight' || mode === 'tree') && knownJson) {
|
|
886
|
-
body = h('pre', { class: 'ds-ep-json ds-ep-json-hl', style }, ...highlightJsonSpans(text));
|
|
887
|
-
} else {
|
|
888
|
-
body = h('pre', { class: 'ds-ep-json', style }, text);
|
|
889
|
-
}
|
|
890
|
-
if (!copyable) return body;
|
|
891
|
-
return h('div', { class: 'ds-ep-json-wrap' }, jsonCopyButton(text), body);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
export function IconButtonGroup({ items = [], value, onChange, dense = false } = {}) {
|
|
895
|
-
return h('div', { class: 'ds-ep-btngrp' + (dense ? ' dense' : ''), role: 'group' },
|
|
896
|
-
...items.map((it) => h('button', {
|
|
897
|
-
key: it.id,
|
|
898
|
-
type: 'button',
|
|
899
|
-
class: 'ds-ep-btngrp-btn' + (it.id === value ? ' active' : ''),
|
|
900
|
-
title: it.title || it.label || it.id,
|
|
901
|
-
'aria-pressed': it.id === value ? 'true' : 'false',
|
|
902
|
-
disabled: it.disabled ? 'disabled' : null,
|
|
903
|
-
onclick: () => { if (!it.disabled && onChange) onChange(it.id); }
|
|
904
|
-
}, it.glyph != null ? it.glyph : it.label))
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// ---------------------------------------------------------------------------
|
|
909
|
-
// Collapse — progressive-disclosure toggle panel (screen-real-estate
|
|
910
|
-
// density: property inspectors, nested settings, FAQ-style panels).
|
|
911
|
-
// Controlled component: caller owns `expanded` state and passes `onToggle`,
|
|
912
|
-
// same pattern as Drawer/Dialog `open`/`onClose` above — no internal state.
|
|
913
|
-
// ---------------------------------------------------------------------------
|
|
914
|
-
export function Collapse({ title, expanded = false, onToggle, children, key } = {}) {
|
|
915
|
-
return h('div', { key, class: 'ds-ep-collapse' + (expanded ? ' is-expanded' : '') },
|
|
916
|
-
h('button', {
|
|
917
|
-
type: 'button', class: 'ds-ep-collapse-head',
|
|
918
|
-
'aria-expanded': expanded ? 'true' : 'false',
|
|
919
|
-
onclick: () => { if (onToggle) onToggle(!expanded); },
|
|
920
|
-
},
|
|
921
|
-
h('span', { class: 'ds-ep-collapse-chevron' }, expanded ? 'v' : '>'),
|
|
922
|
-
h('span', { class: 'ds-ep-collapse-title' }, title)),
|
|
923
|
-
expanded ? h('div', { class: 'ds-ep-collapse-body' }, children) : null);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// ---------------------------------------------------------------------------
|
|
927
|
-
// CollapseGroup — accordion wrapper enforcing single-open-at-a-time when
|
|
928
|
-
// `accordion=true` (default false — group just lays out children, each
|
|
929
|
-
// Collapse still individually controlled). `openId`/`onOpenChange` drive the
|
|
930
|
-
// accordion; `items` is [{id, title, children}].
|
|
931
|
-
// ---------------------------------------------------------------------------
|
|
932
|
-
export function CollapseGroup({ items = [], openId, onOpenChange, accordion = false, key } = {}) {
|
|
933
|
-
return h('div', { key, class: 'ds-ep-collapse-group' },
|
|
934
|
-
...items.map((it) => Collapse({
|
|
935
|
-
key: it.id,
|
|
936
|
-
title: it.title,
|
|
937
|
-
expanded: accordion ? it.id === openId : Boolean(it.expanded),
|
|
938
|
-
onToggle: (next) => {
|
|
939
|
-
if (!onOpenChange) return;
|
|
940
|
-
if (accordion) onOpenChange(next ? it.id : null);
|
|
941
|
-
else onOpenChange(it.id, next);
|
|
942
|
-
},
|
|
943
|
-
children: it.children,
|
|
944
|
-
})));
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// ---------------------------------------------------------------------------
|
|
948
|
-
// Divider — plain rule, optional centered text label, optional vertical
|
|
949
|
-
// orientation (for segmenting dense panels without a full Section wrapper).
|
|
950
|
-
// ---------------------------------------------------------------------------
|
|
951
|
-
export function Divider({ label, vertical = false, key } = {}) {
|
|
952
|
-
if (vertical) return h('span', { key, class: 'ds-ep-divider ds-ep-divider-vertical', role: 'separator', 'aria-orientation': 'vertical' });
|
|
953
|
-
if (!label) return h('hr', { key, class: 'ds-ep-divider' });
|
|
954
|
-
return h('div', { key, class: 'ds-ep-divider ds-ep-divider-labeled', role: 'separator' },
|
|
955
|
-
h('span', { class: 'ds-ep-divider-label' }, label));
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// ---------------------------------------------------------------------------
|
|
959
|
-
// InfoRow / InfoSection / DiagnosticsPanel — static debug/system-info
|
|
960
|
-
// readouts: a bordered section of label + monospace-value rows. Ported from
|
|
961
|
-
// docstudio's diagnostics page (auth/streaming/service-worker state, recent
|
|
962
|
-
// client errors, environment facts). Distinct from PropertyGrid, which is
|
|
963
|
-
// for EDITABLE properties — these rows are read-only display, never inputs.
|
|
964
|
-
// `data == null` (not yet loaded) renders a loading placeholder row instead
|
|
965
|
-
// of an empty section, so a panel never flashes an empty bordered box before
|
|
966
|
-
// its first data arrives; `onRefresh` renders a trailing refresh button that
|
|
967
|
-
// reuses the section's own header row rather than shifting layout beneath it.
|
|
968
|
-
// ---------------------------------------------------------------------------
|
|
969
|
-
export function InfoRow({ label, value, key } = {}) {
|
|
970
|
-
return h('div', { key, class: 'ds-ep-inforow' },
|
|
971
|
-
h('span', { class: 'ds-ep-inforow-label' }, label),
|
|
972
|
-
h('span', { class: 'ds-ep-inforow-value' }, value == null || value === '' ? '—' : String(value)));
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
export function InfoSection({ title, rows, key } = {}) {
|
|
976
|
-
const children = [
|
|
977
|
-
title ? h('h3', { key: 'title', class: 'ds-ep-infosection-title' }, title) : null,
|
|
978
|
-
rows == null
|
|
979
|
-
? h('div', { key: 'body-loading', class: 'ds-ep-infosection-loading', role: 'status' }, 'Loading…')
|
|
980
|
-
: h('div', { key: 'body-rows', class: 'ds-ep-infosection-rows' }, ...rows.map((r, i) => InfoRow({ ...r, key: r.key != null ? r.key : i })))
|
|
981
|
-
].filter(Boolean);
|
|
982
|
-
return h('section', { key, class: 'ds-ep-infosection' }, ...children);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
export function DiagnosticsPanel({ title = 'Diagnostics', sections = [], onRefresh, refreshing = false, key } = {}) {
|
|
986
|
-
const headChildren = [
|
|
987
|
-
h('h2', { key: 'title', class: 'ds-ep-diagnostics-title' }, title),
|
|
988
|
-
onRefresh ? h('button', {
|
|
989
|
-
key: 'refresh', type: 'button', class: 'ds-ep-diagnostics-refresh', disabled: refreshing, 'aria-busy': refreshing ? 'true' : 'false',
|
|
990
|
-
onclick: () => onRefresh()
|
|
991
|
-
}, refreshing ? 'Refreshing…' : 'Refresh') : null
|
|
992
|
-
].filter(Boolean);
|
|
993
|
-
return h('div', { key, class: 'ds-ep-diagnostics' },
|
|
994
|
-
h('div', { key: 'head', class: 'ds-ep-diagnostics-head' }, ...headChildren),
|
|
995
|
-
...sections.map((s, i) => InfoSection({ ...s, key: s.key || i }))
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// ---------------------------------------------------------------------------
|
|
1000
|
-
// BatchProgressLabel — sequential batch-operation progress readout: "label
|
|
1001
|
-
// (i/n)" while in flight. Ported from docstudio's sequential upload badge
|
|
1002
|
-
// and bulk-action progress button (both show a live count against a total
|
|
1003
|
-
// while processing one item at a time). Pure display — the host owns the
|
|
1004
|
-
// actual queue/loop; this only renders its current {done, total} state.
|
|
1005
|
-
// done === 0 renders the bare label with no count suffix (nothing has
|
|
1006
|
-
// happened yet); done === total renders as complete with no live-region
|
|
1007
|
-
// churn once settled.
|
|
1008
|
-
// ---------------------------------------------------------------------------
|
|
1009
|
-
export function BatchProgressLabel({ label = 'Processing', done = 0, total = 0, key } = {}) {
|
|
1010
|
-
const inFlight = total > 0 && done < total;
|
|
1011
|
-
const suffix = total > 0 ? ` (${done}/${total})` : '';
|
|
1012
|
-
return h('span', { key, class: 'ds-ep-batchprogress', role: 'status', 'aria-live': inFlight ? 'polite' : 'off' },
|
|
1013
|
-
label + suffix);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// formatBatchOutcome — pure string helper for the aggregate toast shown once
|
|
1017
|
-
// a batch settles: "N/total succeeded" plus a truncated failed-name list
|
|
1018
|
-
// when any items failed, matching docstudio's attach-bar/bulk-action
|
|
1019
|
-
// aggregation copy. Handles all three outcome shapes (all-succeed, all-fail,
|
|
1020
|
-
// partial) and truncates a long failed-name list ("...and N more") instead
|
|
1021
|
-
// of growing the toast unboundedly for a big batch.
|
|
1022
|
-
export function formatBatchOutcome({ succeeded = 0, total = 0, failedNames = [], maxNames = 3 } = {}) {
|
|
1023
|
-
if (total === 0) return '';
|
|
1024
|
-
if (failedNames.length === 0) return `${succeeded}/${total} succeeded`;
|
|
1025
|
-
const shown = failedNames.slice(0, maxNames).join(', ');
|
|
1026
|
-
const more = failedNames.length > maxNames ? ` and ${failedNames.length - maxNames} more` : '';
|
|
1027
|
-
return `${succeeded}/${total} succeeded; failed: ${shown}${more}`;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// runBatchSequential — pure async orchestration companion to
|
|
1031
|
-
// BatchProgressLabel/formatBatchOutcome: runs `items` through `fn` one at a
|
|
1032
|
-
// time (never in parallel, matching docstudio's rate-limited bulk-action
|
|
1033
|
-
// runner), never aborting on a single item's failure. `onProgress({done,
|
|
1034
|
-
// total})` fires after each item settles so a host can drive
|
|
1035
|
-
// BatchProgressLabel live; the final `{succeeded, total, failedNames}`
|
|
1036
|
-
// return shape feeds formatBatchOutcome directly. `fn` receives (item, index)
|
|
1037
|
-
// and may reject/throw — a rejection is recorded as a failure keyed by
|
|
1038
|
-
// `item.name != null ? item.name : String(item)`, never re-thrown.
|
|
1039
|
-
export async function runBatchSequential(items = [], fn, onProgress) {
|
|
1040
|
-
const total = items.length;
|
|
1041
|
-
let succeeded = 0;
|
|
1042
|
-
const failedNames = [];
|
|
1043
|
-
for (let i = 0; i < total; i += 1) {
|
|
1044
|
-
const item = items[i];
|
|
1045
|
-
try {
|
|
1046
|
-
await fn(item, i);
|
|
1047
|
-
succeeded += 1;
|
|
1048
|
-
} catch (err) {
|
|
1049
|
-
failedNames.push(item && item.name != null ? item.name : String(item));
|
|
1050
|
-
}
|
|
1051
|
-
if (onProgress) onProgress({ done: i + 1, total });
|
|
1052
|
-
}
|
|
1053
|
-
return { succeeded, total, failedNames };
|
|
1054
|
-
}
|
|
7
|
+
// This module is a barrel: every component lives in a single-responsibility
|
|
8
|
+
// submodule under ./editor-primitives/, and the public export surface here is
|
|
9
|
+
// unchanged — no consumer import needs to move.
|
|
10
|
+
|
|
11
|
+
import { Toolbar, ToolbarRow, Tabs, IconButtonGroup } from './editor-primitives/chrome.js';
|
|
12
|
+
import { TreeView, TreeItem } from './editor-primitives/tree.js';
|
|
13
|
+
import { PropertyGrid, PropertyField, PropertyGridRow, InlineEditableField } from './editor-primitives/property-grid.js';
|
|
14
|
+
import { Dock, BP_SM, BP_MD, BP_LG, BP_XL, useMediaQuery, Grid, GridItem, Divider } from './editor-primitives/layout.js';
|
|
15
|
+
import { ResizeHandle, SplitPanel } from './editor-primitives/split-panel.js';
|
|
16
|
+
import { Collapse, CollapseGroup } from './editor-primitives/collapse.js';
|
|
17
|
+
import { FocusTrap } from './editor-primitives/focus-trap.js';
|
|
18
|
+
import { ContextMenu, useContextMenu } from './editor-primitives/context-menu.js';
|
|
19
|
+
import { Drawer, Dialog } from './editor-primitives/modals.js';
|
|
20
|
+
import { Toast, toast } from './editor-primitives/toast.js';
|
|
21
|
+
import { Pager } from './editor-primitives/pager.js';
|
|
22
|
+
import { JsonViewer } from './editor-primitives/json-viewer.js';
|
|
23
|
+
import { InfoRow, InfoSection, DiagnosticsPanel } from './editor-primitives/diagnostics.js';
|
|
24
|
+
import { BatchProgressLabel, formatBatchOutcome, runBatchSequential } from './editor-primitives/batch.js';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
Toolbar, ToolbarRow, Tabs, IconButtonGroup,
|
|
28
|
+
TreeView, TreeItem,
|
|
29
|
+
PropertyGrid, PropertyField, PropertyGridRow, InlineEditableField,
|
|
30
|
+
Dock, BP_SM, BP_MD, BP_LG, BP_XL, useMediaQuery, Grid, GridItem, Divider,
|
|
31
|
+
ResizeHandle, SplitPanel,
|
|
32
|
+
Collapse, CollapseGroup,
|
|
33
|
+
FocusTrap,
|
|
34
|
+
ContextMenu, useContextMenu,
|
|
35
|
+
Drawer, Dialog,
|
|
36
|
+
Toast, toast,
|
|
37
|
+
Pager,
|
|
38
|
+
JsonViewer,
|
|
39
|
+
InfoRow, InfoSection, DiagnosticsPanel,
|
|
40
|
+
BatchProgressLabel, formatBatchOutcome, runBatchSequential,
|
|
41
|
+
};
|