pith-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +67 -0
- package/API.md +3281 -0
- package/LICENSE +661 -0
- package/README.md +80 -2
- package/dist/composer.d.ts +38 -0
- package/dist/composer.d.ts.map +1 -0
- package/dist/composer.js +114 -0
- package/dist/composer.js.map +1 -0
- package/dist/content/conversation-reader.d.ts +19 -0
- package/dist/content/conversation-reader.d.ts.map +1 -0
- package/dist/content/conversation-reader.js +280 -0
- package/dist/content/conversation-reader.js.map +1 -0
- package/dist/content/conversation-view.d.ts +48 -0
- package/dist/content/conversation-view.d.ts.map +1 -0
- package/dist/content/conversation-view.js +107 -0
- package/dist/content/conversation-view.js.map +1 -0
- package/dist/content/image-viewer.d.ts +23 -0
- package/dist/content/image-viewer.d.ts.map +1 -0
- package/dist/content/image-viewer.js +101 -0
- package/dist/content/image-viewer.js.map +1 -0
- package/dist/content/markdown.d.ts +12 -0
- package/dist/content/markdown.d.ts.map +1 -0
- package/dist/content/markdown.js +68 -0
- package/dist/content/markdown.js.map +1 -0
- package/dist/content/paged-preview.d.ts +15 -0
- package/dist/content/paged-preview.d.ts.map +1 -0
- package/dist/content/paged-preview.js +105 -0
- package/dist/content/paged-preview.js.map +1 -0
- package/dist/content/views.d.ts +9 -0
- package/dist/content/views.d.ts.map +1 -0
- package/dist/content/views.js +53 -0
- package/dist/content/views.js.map +1 -0
- package/dist/content/welcome.d.ts +18 -0
- package/dist/content/welcome.d.ts.map +1 -0
- package/dist/content/welcome.js +45 -0
- package/dist/content/welcome.js.map +1 -0
- package/dist/core.d.ts +36 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +44 -0
- package/dist/core.js.map +1 -0
- package/dist/dom.d.ts +14 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +69 -0
- package/dist/dom.js.map +1 -0
- package/dist/files/panels.d.ts +95 -0
- package/dist/files/panels.d.ts.map +1 -0
- package/dist/files/panels.js +245 -0
- package/dist/files/panels.js.map +1 -0
- package/dist/files/systems.d.ts +91 -0
- package/dist/files/systems.d.ts.map +1 -0
- package/dist/files/systems.js +256 -0
- package/dist/files/systems.js.map +1 -0
- package/dist/forms/actions.d.ts +27 -0
- package/dist/forms/actions.d.ts.map +1 -0
- package/dist/forms/actions.js +19 -0
- package/dist/forms/actions.js.map +1 -0
- package/dist/forms/custom-connection.d.ts +61 -0
- package/dist/forms/custom-connection.d.ts.map +1 -0
- package/dist/forms/custom-connection.js +65 -0
- package/dist/forms/custom-connection.js.map +1 -0
- package/dist/forms/dialog-frame.d.ts +39 -0
- package/dist/forms/dialog-frame.d.ts.map +1 -0
- package/dist/forms/dialog-frame.js +75 -0
- package/dist/forms/dialog-frame.js.map +1 -0
- package/dist/forms/history.d.ts +16 -0
- package/dist/forms/history.d.ts.map +1 -0
- package/dist/forms/history.js +21 -0
- package/dist/forms/history.js.map +1 -0
- package/dist/forms/identity.d.ts +4 -0
- package/dist/forms/identity.d.ts.map +1 -0
- package/dist/forms/identity.js +21 -0
- package/dist/forms/identity.js.map +1 -0
- package/dist/forms/model-editor.d.ts +35 -0
- package/dist/forms/model-editor.d.ts.map +1 -0
- package/dist/forms/model-editor.js +78 -0
- package/dist/forms/model-editor.js.map +1 -0
- package/dist/forms/panels.d.ts +57 -0
- package/dist/forms/panels.d.ts.map +1 -0
- package/dist/forms/panels.js +224 -0
- package/dist/forms/panels.js.map +1 -0
- package/dist/forms/provider-login.d.ts +22 -0
- package/dist/forms/provider-login.d.ts.map +1 -0
- package/dist/forms/provider-login.js +114 -0
- package/dist/forms/provider-login.js.map +1 -0
- package/dist/forms/resource-editor.d.ts +23 -0
- package/dist/forms/resource-editor.d.ts.map +1 -0
- package/dist/forms/resource-editor.js +24 -0
- package/dist/forms/resource-editor.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context-menu.d.ts +25 -0
- package/dist/navigation/context-menu.d.ts.map +1 -0
- package/dist/navigation/context-menu.js +67 -0
- package/dist/navigation/context-menu.js.map +1 -0
- package/dist/navigation/navigation.d.ts +57 -0
- package/dist/navigation/navigation.d.ts.map +1 -0
- package/dist/navigation/navigation.js +185 -0
- package/dist/navigation/navigation.js.map +1 -0
- package/dist/primitives.d.ts +307 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +741 -0
- package/dist/primitives.js.map +1 -0
- package/dist/styles.css +849 -0
- package/dist/tasks/approval.d.ts +25 -0
- package/dist/tasks/approval.d.ts.map +1 -0
- package/dist/tasks/approval.js +61 -0
- package/dist/tasks/approval.js.map +1 -0
- package/dist/tasks/permission.d.ts +35 -0
- package/dist/tasks/permission.d.ts.map +1 -0
- package/dist/tasks/permission.js +67 -0
- package/dist/tasks/permission.js.map +1 -0
- package/dist/tasks/runtime.d.ts +31 -0
- package/dist/tasks/runtime.d.ts.map +1 -0
- package/dist/tasks/runtime.js +34 -0
- package/dist/tasks/runtime.js.map +1 -0
- package/dist/theme.d.ts +4 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +6 -0
- package/dist/theme.js.map +1 -0
- package/dist/workspace.d.ts +38 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +183 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +27 -4
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { Generation, Lifetime, setControlPending } from './core.js';
|
|
2
|
+
import { element, focusableElements, iconElement, setText, uniqueId } from './dom.js';
|
|
3
|
+
export function createButton(options) {
|
|
4
|
+
const node = element('button', 'pui-button');
|
|
5
|
+
node.type = options.type ?? 'button';
|
|
6
|
+
const caption = element('span');
|
|
7
|
+
node.append(caption);
|
|
8
|
+
const life = new Lifetime();
|
|
9
|
+
const generation = new Generation();
|
|
10
|
+
let state = options;
|
|
11
|
+
let localPending = false;
|
|
12
|
+
let currentIcon = '';
|
|
13
|
+
let glyph;
|
|
14
|
+
const update = (next) => {
|
|
15
|
+
state = next;
|
|
16
|
+
setText(caption, next.label);
|
|
17
|
+
node.className = `pui-button pui-${next.intent ?? 'neutral'} pui-${next.density ?? 'normal'}`;
|
|
18
|
+
node.disabled = !!(next.disabled || next.pending || localPending);
|
|
19
|
+
setControlPending(node, !!(next.pending || localPending));
|
|
20
|
+
node.setAttribute('aria-busy', String(!!(next.pending || localPending)));
|
|
21
|
+
node.title = next.title ?? '';
|
|
22
|
+
if (currentIcon !== (next.icon ?? '')) {
|
|
23
|
+
glyph?.remove();
|
|
24
|
+
currentIcon = next.icon ?? '';
|
|
25
|
+
glyph = next.icon ? iconElement(next.icon) : undefined;
|
|
26
|
+
if (glyph)
|
|
27
|
+
node.prepend(glyph);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
life.listen(node, 'click', (() => {
|
|
31
|
+
if (!options.onAction || node.disabled)
|
|
32
|
+
return;
|
|
33
|
+
const seq = generation.next();
|
|
34
|
+
localPending = true;
|
|
35
|
+
update(state);
|
|
36
|
+
const complete = (result) => {
|
|
37
|
+
if (generation.current(seq) && result && !result.ok)
|
|
38
|
+
options.onError?.(result.error);
|
|
39
|
+
};
|
|
40
|
+
const failed = (error) => { if (generation.current(seq))
|
|
41
|
+
options.onError?.(error instanceof Error ? error.message : String(error)); };
|
|
42
|
+
const finish = () => { if (generation.current(seq)) {
|
|
43
|
+
localPending = false;
|
|
44
|
+
update(state);
|
|
45
|
+
} };
|
|
46
|
+
try {
|
|
47
|
+
const result = options.onAction();
|
|
48
|
+
if (result && 'then' in result)
|
|
49
|
+
void result.then(complete).catch(failed).finally(finish);
|
|
50
|
+
else {
|
|
51
|
+
complete(result);
|
|
52
|
+
finish();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
failed(error);
|
|
57
|
+
finish();
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
update(options);
|
|
61
|
+
return { element: node, update, destroy() { generation.destroy(); life.destroy(); } };
|
|
62
|
+
}
|
|
63
|
+
export function createIconButton(options) {
|
|
64
|
+
const block = createButton(options);
|
|
65
|
+
const update = block.update;
|
|
66
|
+
block.update = state => {
|
|
67
|
+
update(state);
|
|
68
|
+
block.element.classList.add('pui-icon-button');
|
|
69
|
+
block.element.setAttribute('aria-label', state.label);
|
|
70
|
+
block.element.querySelector('span')?.classList.add('pui-sr-only');
|
|
71
|
+
};
|
|
72
|
+
block.update(options);
|
|
73
|
+
return block;
|
|
74
|
+
}
|
|
75
|
+
export function createTextAction(options) {
|
|
76
|
+
const block = createButton(options);
|
|
77
|
+
const update = block.update;
|
|
78
|
+
block.update = state => { update(state); block.element.classList.add('pui-text-action'); };
|
|
79
|
+
block.update(options);
|
|
80
|
+
return block;
|
|
81
|
+
}
|
|
82
|
+
export function createIcon(state) {
|
|
83
|
+
const root = element('span', 'pui-icon-display');
|
|
84
|
+
let currentName = '';
|
|
85
|
+
const update = (next) => {
|
|
86
|
+
if (next.name !== currentName) {
|
|
87
|
+
currentName = next.name;
|
|
88
|
+
root.replaceChildren(iconElement(next.name));
|
|
89
|
+
}
|
|
90
|
+
root.classList.toggle('pui-icon-small', next.size === 'small');
|
|
91
|
+
if (next.label) {
|
|
92
|
+
root.setAttribute('role', 'img');
|
|
93
|
+
root.setAttribute('aria-label', next.label);
|
|
94
|
+
root.removeAttribute('aria-hidden');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
root.removeAttribute('role');
|
|
98
|
+
root.removeAttribute('aria-label');
|
|
99
|
+
root.setAttribute('aria-hidden', 'true');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
update(state);
|
|
103
|
+
return { element: root, update, destroy() { } };
|
|
104
|
+
}
|
|
105
|
+
export function createDivider(state = {}) {
|
|
106
|
+
const root = element('div', 'pui-divider');
|
|
107
|
+
root.setAttribute('role', 'separator');
|
|
108
|
+
const update = (next) => { setText(root, next.label); if (next.label)
|
|
109
|
+
root.setAttribute('aria-label', next.label);
|
|
110
|
+
else
|
|
111
|
+
root.removeAttribute('aria-label'); };
|
|
112
|
+
update(state);
|
|
113
|
+
return { element: root, update, destroy() { } };
|
|
114
|
+
}
|
|
115
|
+
export function createKeyValue(options) {
|
|
116
|
+
const root = element('dl', 'pui-key-value');
|
|
117
|
+
const rows = new Map();
|
|
118
|
+
const update = (state) => {
|
|
119
|
+
const keep = new Set(state.items.map(item => item.key));
|
|
120
|
+
for (const [key, row] of rows)
|
|
121
|
+
if (!keep.has(key)) {
|
|
122
|
+
row.node.remove();
|
|
123
|
+
rows.delete(key);
|
|
124
|
+
}
|
|
125
|
+
for (const item of state.items) {
|
|
126
|
+
let row = rows.get(item.key);
|
|
127
|
+
if (!row) {
|
|
128
|
+
const label = element('dt');
|
|
129
|
+
const value = element('dd');
|
|
130
|
+
row = { node: element('div', '', label, value), label, value };
|
|
131
|
+
rows.set(item.key, row);
|
|
132
|
+
}
|
|
133
|
+
setText(row.label, item.label);
|
|
134
|
+
setText(row.value, item.value);
|
|
135
|
+
root.append(row.node);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
update(options);
|
|
139
|
+
return { element: root, update, destroy() { rows.clear(); } };
|
|
140
|
+
}
|
|
141
|
+
export function createToolbar(options) {
|
|
142
|
+
const root = element('div', 'pui-toolbar');
|
|
143
|
+
root.setAttribute('role', 'group');
|
|
144
|
+
root.append(...(options.children ?? []));
|
|
145
|
+
const update = (state) => { root.setAttribute('aria-label', state.label); root.classList.toggle('pui-compact', state.density === 'compact'); };
|
|
146
|
+
update(options);
|
|
147
|
+
return { element: root, update, destroy() { } };
|
|
148
|
+
}
|
|
149
|
+
export function createScrollArea(options) {
|
|
150
|
+
const root = element('div', 'pui-scroll-area', options.content);
|
|
151
|
+
root.tabIndex = 0;
|
|
152
|
+
root.setAttribute('role', 'region');
|
|
153
|
+
const update = (state) => { root.setAttribute('aria-label', state.label); root.dataset.size = state.size ?? 'medium'; };
|
|
154
|
+
update(options);
|
|
155
|
+
return { element: root, update, destroy() { } };
|
|
156
|
+
}
|
|
157
|
+
export function createField(options) {
|
|
158
|
+
const root = element('div', 'pui-field');
|
|
159
|
+
const label = element('label', 'pui-field-label');
|
|
160
|
+
const description = element('div', 'pui-field-description');
|
|
161
|
+
const error = element('div', 'pui-field-error');
|
|
162
|
+
const control = options.control;
|
|
163
|
+
if (!control.id)
|
|
164
|
+
control.id = uniqueId('pui-input');
|
|
165
|
+
label.htmlFor = control.id;
|
|
166
|
+
description.id = uniqueId('pui-description');
|
|
167
|
+
error.id = uniqueId('pui-error');
|
|
168
|
+
error.setAttribute('role', 'alert');
|
|
169
|
+
control.classList.add('pui-control');
|
|
170
|
+
root.append(label, control, description, error);
|
|
171
|
+
const update = (state) => {
|
|
172
|
+
setText(label, `${state.label}${state.required ? ' *' : ''}`);
|
|
173
|
+
label.hidden = !state.label;
|
|
174
|
+
setText(description, state.description);
|
|
175
|
+
description.hidden = !state.description;
|
|
176
|
+
setText(error, state.error);
|
|
177
|
+
error.hidden = !state.error;
|
|
178
|
+
control.disabled = !!state.disabled;
|
|
179
|
+
control.required = !!state.required;
|
|
180
|
+
control.setAttribute('aria-invalid', String(!!state.error));
|
|
181
|
+
const describedBy = [state.description ? description.id : '', state.error ? error.id : ''].filter(Boolean).join(' ');
|
|
182
|
+
if (describedBy)
|
|
183
|
+
control.setAttribute('aria-describedby', describedBy);
|
|
184
|
+
else
|
|
185
|
+
control.removeAttribute('aria-describedby');
|
|
186
|
+
root.classList.toggle('pui-invalid', !!state.error);
|
|
187
|
+
};
|
|
188
|
+
update(options);
|
|
189
|
+
return { element: root, control, update, destroy() { } };
|
|
190
|
+
}
|
|
191
|
+
function textControl(options, type) {
|
|
192
|
+
const control = type === 'textarea' ? element('textarea') : element('input');
|
|
193
|
+
if (control instanceof HTMLInputElement) {
|
|
194
|
+
control.type = type === 'textarea' ? 'text' : type;
|
|
195
|
+
if (options.min != null)
|
|
196
|
+
control.min = String(options.min);
|
|
197
|
+
if (options.max != null)
|
|
198
|
+
control.max = String(options.max);
|
|
199
|
+
if (options.step != null)
|
|
200
|
+
control.step = String(options.step);
|
|
201
|
+
}
|
|
202
|
+
if (options.autocomplete)
|
|
203
|
+
control.setAttribute('autocomplete', options.autocomplete);
|
|
204
|
+
control.value = options.value ?? '';
|
|
205
|
+
const field = createField({ ...options, control });
|
|
206
|
+
const life = new Lifetime();
|
|
207
|
+
life.listen(control, 'input', (event => options.onInput?.(control.value, event)));
|
|
208
|
+
life.listen(control, 'change', (event => options.onChange?.(control.value, event)));
|
|
209
|
+
const update = (state) => {
|
|
210
|
+
field.update(state);
|
|
211
|
+
control.placeholder = state.placeholder ?? '';
|
|
212
|
+
control.readOnly = !!state.readOnly;
|
|
213
|
+
control.classList.toggle('pui-compact', state.density === 'compact');
|
|
214
|
+
};
|
|
215
|
+
update(options);
|
|
216
|
+
return { element: field.element, control, update, getValue: () => control.value,
|
|
217
|
+
reset(value) { if (control.value !== value)
|
|
218
|
+
control.value = value; }, focus() { control.focus(); },
|
|
219
|
+
destroy() { life.destroy(); field.destroy(); } };
|
|
220
|
+
}
|
|
221
|
+
export const createTextInput = (options) => textControl(options, 'text');
|
|
222
|
+
export const createSearchInput = (options) => textControl(options, 'search');
|
|
223
|
+
export const createNumberInput = (options) => textControl(options, 'number');
|
|
224
|
+
export const createTextArea = (options) => textControl(options, 'textarea');
|
|
225
|
+
export function createSecretInput(options) {
|
|
226
|
+
const block = textControl({ ...options, autocomplete: options.autocomplete ?? 'off' }, 'password');
|
|
227
|
+
const control = block.control;
|
|
228
|
+
const toggle = createTextAction({ label: 'Show', onAction() {
|
|
229
|
+
const show = control.type === 'password';
|
|
230
|
+
control.type = show ? 'text' : 'password';
|
|
231
|
+
toggle.update({ label: show ? 'Hide' : 'Show', disabled: control.disabled });
|
|
232
|
+
toggle.element.setAttribute('aria-pressed', String(show));
|
|
233
|
+
} });
|
|
234
|
+
toggle.element.setAttribute('aria-controls', control.id);
|
|
235
|
+
toggle.element.setAttribute('aria-pressed', 'false');
|
|
236
|
+
block.element.append(toggle.element);
|
|
237
|
+
const update = block.update;
|
|
238
|
+
block.update = state => { update(state); toggle.update({ label: control.type === 'password' ? 'Show' : 'Hide', disabled: state.disabled }); };
|
|
239
|
+
const destroy = block.destroy;
|
|
240
|
+
block.destroy = () => { destroy(); toggle.destroy(); };
|
|
241
|
+
return block;
|
|
242
|
+
}
|
|
243
|
+
export function createSelect(options) {
|
|
244
|
+
const control = element('select');
|
|
245
|
+
control.multiple = !!options.multiple;
|
|
246
|
+
if (options.size)
|
|
247
|
+
control.size = options.size;
|
|
248
|
+
const field = createField({ ...options, control });
|
|
249
|
+
const life = new Lifetime();
|
|
250
|
+
let signature = '';
|
|
251
|
+
const update = (state) => {
|
|
252
|
+
field.update(state);
|
|
253
|
+
control.classList.toggle('pui-compact', state.density === 'compact');
|
|
254
|
+
const next = JSON.stringify(state.options);
|
|
255
|
+
if (next !== signature) {
|
|
256
|
+
signature = next;
|
|
257
|
+
const selected = new Set([...control.selectedOptions].map(option => option.value));
|
|
258
|
+
const nodes = state.options.map(item => {
|
|
259
|
+
const option = element('option', '', item.label);
|
|
260
|
+
option.value = item.value;
|
|
261
|
+
option.disabled = !!item.disabled;
|
|
262
|
+
option.selected = selected.has(item.value);
|
|
263
|
+
return option;
|
|
264
|
+
});
|
|
265
|
+
control.replaceChildren(...nodes);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
life.listen(control, 'change', (() => options.onChange?.(control.value)));
|
|
269
|
+
update(options);
|
|
270
|
+
if (options.value != null)
|
|
271
|
+
control.value = options.value;
|
|
272
|
+
return { element: field.element, control, update, getValue: () => control.value,
|
|
273
|
+
reset(value) { control.value = value; }, focus() { control.focus(); }, destroy() { life.destroy(); } };
|
|
274
|
+
}
|
|
275
|
+
export function createCheckbox(options) {
|
|
276
|
+
const control = element('input');
|
|
277
|
+
control.type = 'checkbox';
|
|
278
|
+
control.checked = !!options.checked;
|
|
279
|
+
const caption = element('span');
|
|
280
|
+
const description = element('small');
|
|
281
|
+
const root = element('label', 'pui-checkbox', control, element('span', '', caption, description));
|
|
282
|
+
const life = new Lifetime();
|
|
283
|
+
const update = (state) => {
|
|
284
|
+
setText(caption, state.label);
|
|
285
|
+
setText(description, state.description);
|
|
286
|
+
description.hidden = !state.description;
|
|
287
|
+
control.disabled = !!state.disabled;
|
|
288
|
+
};
|
|
289
|
+
life.listen(control, 'change', (() => options.onChange?.(control.checked)));
|
|
290
|
+
update(options);
|
|
291
|
+
return { element: root, control, update, getValue: () => control.checked,
|
|
292
|
+
reset(value) { control.checked = value; }, focus() { control.focus(); }, destroy() { life.destroy(); } };
|
|
293
|
+
}
|
|
294
|
+
export function createNotice(state) {
|
|
295
|
+
const root = element('div', 'pui-notice');
|
|
296
|
+
const update = (next) => {
|
|
297
|
+
root.className = `pui-notice pui-notice-${next.tone ?? 'info'}`;
|
|
298
|
+
root.setAttribute('role', next.tone === 'error' || next.tone === 'warning' ? 'alert' : 'status');
|
|
299
|
+
root.hidden = !next.message;
|
|
300
|
+
setText(root, next.message);
|
|
301
|
+
};
|
|
302
|
+
update(state);
|
|
303
|
+
return { element: root, update, destroy() { } };
|
|
304
|
+
}
|
|
305
|
+
export function createStatus(state) {
|
|
306
|
+
const root = element('span', 'pui-status');
|
|
307
|
+
const update = (next) => { root.className = `pui-status pui-status-${next.tone ?? 'neutral'}`; setText(root, next.label); };
|
|
308
|
+
update(state);
|
|
309
|
+
return { element: root, update, destroy() { } };
|
|
310
|
+
}
|
|
311
|
+
export function createProgress(state) {
|
|
312
|
+
const control = element('progress');
|
|
313
|
+
const label = element('span');
|
|
314
|
+
const root = element('label', 'pui-progress', label, control);
|
|
315
|
+
const update = (next) => {
|
|
316
|
+
setText(label, next.label);
|
|
317
|
+
control.max = next.max && next.max > 0 ? next.max : 100;
|
|
318
|
+
if (next.value == null)
|
|
319
|
+
control.removeAttribute('value');
|
|
320
|
+
else
|
|
321
|
+
control.value = Math.max(0, Math.min(next.value, control.max));
|
|
322
|
+
};
|
|
323
|
+
update(state);
|
|
324
|
+
return { element: root, update, destroy() { } };
|
|
325
|
+
}
|
|
326
|
+
export function createFormSection(options) {
|
|
327
|
+
const title = element('h3');
|
|
328
|
+
const description = element('p', 'pui-muted');
|
|
329
|
+
const root = element('section', 'pui-form-section', title, description);
|
|
330
|
+
if (options.content)
|
|
331
|
+
root.append(options.content);
|
|
332
|
+
const update = (state) => { setText(title, state.title); setText(description, state.description); description.hidden = !state.description; };
|
|
333
|
+
update(options);
|
|
334
|
+
return { element: root, update, destroy() { } };
|
|
335
|
+
}
|
|
336
|
+
export function createActions(options = {}) {
|
|
337
|
+
const root = element('div', 'pui-actions');
|
|
338
|
+
root.append(...(options.children ?? []));
|
|
339
|
+
const update = (state) => { root.dataset.align = state.align ?? 'end'; };
|
|
340
|
+
update(options);
|
|
341
|
+
return { element: root, update, destroy() { } };
|
|
342
|
+
}
|
|
343
|
+
export function createDisclosure(options) {
|
|
344
|
+
const root = element('details', 'pui-disclosure');
|
|
345
|
+
const summary = element('summary');
|
|
346
|
+
const title = element('span');
|
|
347
|
+
const description = element('small', 'pui-muted');
|
|
348
|
+
summary.append(title, description);
|
|
349
|
+
root.append(summary, element('div', 'pui-disclosure-content', options.content));
|
|
350
|
+
root.open = !!options.open;
|
|
351
|
+
let disabled = !!options.disabled;
|
|
352
|
+
const life = new Lifetime();
|
|
353
|
+
life.listen(root, 'toggle', (() => options.onToggle?.(root.open)));
|
|
354
|
+
life.listen(summary, 'click', (event => { if (disabled)
|
|
355
|
+
event.preventDefault(); }));
|
|
356
|
+
const update = (state) => {
|
|
357
|
+
setText(title, state.title);
|
|
358
|
+
setText(description, state.description);
|
|
359
|
+
description.hidden = !state.description;
|
|
360
|
+
disabled = !!state.disabled;
|
|
361
|
+
summary.setAttribute('aria-disabled', String(disabled));
|
|
362
|
+
};
|
|
363
|
+
update(options);
|
|
364
|
+
return { element: root, update, setOpen(open) { root.open = open; }, isOpen: () => root.open, destroy() { life.destroy(); } };
|
|
365
|
+
}
|
|
366
|
+
export function createListRow(options) {
|
|
367
|
+
const title = element('div', 'pui-list-row-title');
|
|
368
|
+
const description = element('div', 'pui-muted');
|
|
369
|
+
const meta = element('small', 'pui-muted');
|
|
370
|
+
const glyph = element('span', 'pui-list-row-icon');
|
|
371
|
+
const root = element('div', 'pui-list-row', glyph, element('div', 'pui-list-row-body', title, description), meta);
|
|
372
|
+
if (options.actions)
|
|
373
|
+
root.append(options.actions);
|
|
374
|
+
let iconName = '';
|
|
375
|
+
const update = (state) => {
|
|
376
|
+
setText(title, state.title);
|
|
377
|
+
setText(description, state.description);
|
|
378
|
+
description.hidden = !state.description;
|
|
379
|
+
setText(meta, state.meta);
|
|
380
|
+
meta.hidden = !state.meta;
|
|
381
|
+
root.classList.toggle('pui-selected', !!state.selected);
|
|
382
|
+
if (iconName !== (state.icon ?? '')) {
|
|
383
|
+
iconName = state.icon ?? '';
|
|
384
|
+
glyph.replaceChildren(...(state.icon ? [iconElement(state.icon)] : []));
|
|
385
|
+
}
|
|
386
|
+
glyph.hidden = !state.icon;
|
|
387
|
+
};
|
|
388
|
+
update(options);
|
|
389
|
+
return { element: root, update, destroy() { } };
|
|
390
|
+
}
|
|
391
|
+
export function createCollectionState(options) {
|
|
392
|
+
const message = createNotice({ message: '' });
|
|
393
|
+
const content = element('div', 'pui-collection-content');
|
|
394
|
+
if (options.content)
|
|
395
|
+
content.append(options.content);
|
|
396
|
+
const retry = createButton({ label: options.retryLabel ?? 'Retry', onAction: options.onRetry });
|
|
397
|
+
const root = element('section', 'pui-collection', message.element, retry.element, content);
|
|
398
|
+
const update = (state) => {
|
|
399
|
+
root.dataset.state = state.status;
|
|
400
|
+
root.setAttribute('aria-busy', String(state.status === 'loading'));
|
|
401
|
+
message.update({ message: state.message ?? ({ ready: '', loading: 'Loading…', empty: 'Nothing here yet.', error: 'Could not load this content.' }[state.status]), tone: state.status === 'error' ? 'error' : 'info' });
|
|
402
|
+
content.hidden = state.status !== 'ready';
|
|
403
|
+
retry.element.hidden = state.status !== 'error' || !options.onRetry;
|
|
404
|
+
retry.update({ label: state.retryLabel ?? 'Retry' });
|
|
405
|
+
};
|
|
406
|
+
update(options);
|
|
407
|
+
return { element: root, update, destroy() { retry.destroy(); message.destroy(); } };
|
|
408
|
+
}
|
|
409
|
+
export function createDialog(options) {
|
|
410
|
+
const root = options.dialog ?? element('dialog');
|
|
411
|
+
root.classList.add('pui-dialog');
|
|
412
|
+
const title = element('h2');
|
|
413
|
+
title.id = uniqueId('pui-dialog-title');
|
|
414
|
+
root.setAttribute('aria-labelledby', title.id);
|
|
415
|
+
const description = element('p', 'pui-muted');
|
|
416
|
+
description.id = uniqueId('pui-dialog-description');
|
|
417
|
+
const eyebrow = element('span', 'pui-eyebrow');
|
|
418
|
+
const close = createIconButton({ label: `Close ${options.title}`, icon: 'close', onAction() { if (!state.closeDisabled)
|
|
419
|
+
root.close(); } });
|
|
420
|
+
const heading = element('header', 'pui-dialog-heading', element('div', '', eyebrow, title, description), close.element);
|
|
421
|
+
const body = element('div', 'pui-dialog-body');
|
|
422
|
+
if (options.content)
|
|
423
|
+
body.append(options.content);
|
|
424
|
+
root.replaceChildren(heading, body);
|
|
425
|
+
if (options.actions)
|
|
426
|
+
root.append(options.actions);
|
|
427
|
+
const life = new Lifetime();
|
|
428
|
+
let state = options;
|
|
429
|
+
let previousFocus = null;
|
|
430
|
+
let disposed = false;
|
|
431
|
+
const update = (next) => {
|
|
432
|
+
state = next;
|
|
433
|
+
setText(title, next.title);
|
|
434
|
+
setText(description, next.description);
|
|
435
|
+
description.hidden = !next.description;
|
|
436
|
+
setText(eyebrow, next.eyebrow);
|
|
437
|
+
eyebrow.hidden = !next.eyebrow;
|
|
438
|
+
if (next.description)
|
|
439
|
+
root.setAttribute('aria-describedby', description.id);
|
|
440
|
+
else
|
|
441
|
+
root.removeAttribute('aria-describedby');
|
|
442
|
+
close.update({ label: `Close ${next.title}`, icon: 'close', disabled: next.closeDisabled });
|
|
443
|
+
};
|
|
444
|
+
life.listen(root, 'cancel', (event => { if (state.closeDisabled)
|
|
445
|
+
event.preventDefault(); }));
|
|
446
|
+
life.listen(root, 'click', (event => {
|
|
447
|
+
if (!options.closeOnBackdrop || state.closeDisabled || event.target !== root)
|
|
448
|
+
return;
|
|
449
|
+
const pointer = event;
|
|
450
|
+
const rect = root.getBoundingClientRect();
|
|
451
|
+
if (pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom)
|
|
452
|
+
root.close();
|
|
453
|
+
}));
|
|
454
|
+
life.listen(root, 'close', (() => {
|
|
455
|
+
// Native close events are queued; an old close must not affect a reopened dialog.
|
|
456
|
+
if (root.open)
|
|
457
|
+
return;
|
|
458
|
+
options.onClose?.();
|
|
459
|
+
if (previousFocus?.isConnected)
|
|
460
|
+
previousFocus.focus({ preventScroll: true });
|
|
461
|
+
previousFocus = null;
|
|
462
|
+
}));
|
|
463
|
+
update(options);
|
|
464
|
+
return { element: root, body, update,
|
|
465
|
+
open() {
|
|
466
|
+
if (disposed || root.open)
|
|
467
|
+
return;
|
|
468
|
+
previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
469
|
+
if (!root.isConnected)
|
|
470
|
+
document.body.append(root);
|
|
471
|
+
root.showModal();
|
|
472
|
+
}, close() { if (root.open)
|
|
473
|
+
root.close(); },
|
|
474
|
+
destroy() { if (disposed)
|
|
475
|
+
return; disposed = true; if (root.open)
|
|
476
|
+
root.close(); life.destroy(); close.destroy(); root.remove(); } };
|
|
477
|
+
}
|
|
478
|
+
export function createTextEditDialog(options = {}) {
|
|
479
|
+
// The editor has one form-level error, so it does not need the general field's
|
|
480
|
+
// description/error/number/secret machinery. Both paths use the same control CSS.
|
|
481
|
+
const control = element('textarea', 'pui-control');
|
|
482
|
+
control.rows = 8;
|
|
483
|
+
control.id = options.ids?.control ?? uniqueId('pui-input');
|
|
484
|
+
const label = element('label', 'pui-field-label', options.label ?? 'Message');
|
|
485
|
+
label.htmlFor = control.id;
|
|
486
|
+
const field = element('div', 'pui-field', label, control);
|
|
487
|
+
const note = element('p', 'pui-muted');
|
|
488
|
+
const error = createNotice({ message: '', tone: 'error' });
|
|
489
|
+
const cancel = createButton({ label: 'Cancel', onAction() { dialog.close(); } });
|
|
490
|
+
const submit = createButton({ label: options.submitLabel ?? 'Save', intent: 'primary', type: 'submit' });
|
|
491
|
+
const actions = createActions({ children: [cancel.element, submit.element] });
|
|
492
|
+
const form = element('form', 'pui-text-edit', field, note, error.element, actions.element);
|
|
493
|
+
const ids = options.ids;
|
|
494
|
+
if (ids?.form)
|
|
495
|
+
form.id = ids.form;
|
|
496
|
+
if (ids?.submit)
|
|
497
|
+
submit.element.id = ids.submit;
|
|
498
|
+
if (ids?.error)
|
|
499
|
+
error.element.id = ids.error;
|
|
500
|
+
if (ids?.cancel)
|
|
501
|
+
cancel.element.id = ids.cancel;
|
|
502
|
+
const generation = new Generation();
|
|
503
|
+
const life = new Lifetime();
|
|
504
|
+
let pending = false;
|
|
505
|
+
let state = { title: options.title ?? 'Edit message', ...options };
|
|
506
|
+
const dialog = createDialog({ ...state, dialog: options.dialog, content: form, onClose() { generation.invalidate(); pending = false; update(state); options.onClose?.(); } });
|
|
507
|
+
if (ids?.title) {
|
|
508
|
+
dialog.element.querySelector('h2').id = ids.title;
|
|
509
|
+
dialog.element.setAttribute('aria-labelledby', ids.title);
|
|
510
|
+
}
|
|
511
|
+
const update = (next) => {
|
|
512
|
+
state = next;
|
|
513
|
+
dialog.update(next);
|
|
514
|
+
setText(note, next.note);
|
|
515
|
+
note.hidden = !next.note;
|
|
516
|
+
error.update({ message: next.error ?? '', tone: 'error' });
|
|
517
|
+
submit.update({ label: next.submitLabel ?? options.submitLabel ?? 'Save', intent: 'primary', disabled: next.disabled, pending: !!(next.pending || pending) });
|
|
518
|
+
control.disabled = !!next.inputDisabled;
|
|
519
|
+
};
|
|
520
|
+
life.listen(control, 'input', () => options.onInput?.(control.value));
|
|
521
|
+
life.listen(form, 'submit', (event => {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
if (pending || state.pending || state.disabled || submit.element.disabled || !options.onSubmit)
|
|
524
|
+
return;
|
|
525
|
+
const seq = generation.next();
|
|
526
|
+
const value = control.value;
|
|
527
|
+
pending = true;
|
|
528
|
+
update(state);
|
|
529
|
+
void Promise.resolve().then(() => options.onSubmit(value)).then(result => {
|
|
530
|
+
if (!generation.current(seq) || !dialog.element.open)
|
|
531
|
+
return;
|
|
532
|
+
if (result.ok)
|
|
533
|
+
dialog.close();
|
|
534
|
+
else {
|
|
535
|
+
state = { ...state, error: result.error };
|
|
536
|
+
}
|
|
537
|
+
}).catch((reason) => {
|
|
538
|
+
if (generation.current(seq))
|
|
539
|
+
state = { ...state, error: reason instanceof Error ? reason.message : String(reason) };
|
|
540
|
+
}).finally(() => { if (generation.current(seq)) {
|
|
541
|
+
pending = false;
|
|
542
|
+
update(state);
|
|
543
|
+
} });
|
|
544
|
+
}));
|
|
545
|
+
update(state);
|
|
546
|
+
return { element: dialog.element, control, submitButton: submit.element, update,
|
|
547
|
+
open(input) {
|
|
548
|
+
generation.invalidate();
|
|
549
|
+
pending = false;
|
|
550
|
+
update({ ...state, error: '', title: input.title ?? options.title ?? 'Edit message', description: input.description ?? options.description, note: input.note ?? options.note });
|
|
551
|
+
control.value = input.value;
|
|
552
|
+
dialog.open();
|
|
553
|
+
control.focus();
|
|
554
|
+
}, close() { generation.invalidate(); dialog.close(); }, getValue: () => control.value, focus() { control.focus(); },
|
|
555
|
+
destroy() { generation.destroy(); life.destroy(); dialog.destroy(); cancel.destroy(); submit.destroy(); } };
|
|
556
|
+
}
|
|
557
|
+
export function createConfirmDialog(options) {
|
|
558
|
+
const message = element('p', 'pui-modal-description');
|
|
559
|
+
const hint = element('p', 'pui-field-hint');
|
|
560
|
+
const error = createNotice({ message: '', tone: 'error' });
|
|
561
|
+
const generation = new Generation();
|
|
562
|
+
let state = options;
|
|
563
|
+
let pending = false;
|
|
564
|
+
const cancel = createButton({ label: 'Cancel', onAction() { dialog.close(); } });
|
|
565
|
+
const confirm = createButton({ label: options.confirmLabel ?? 'Remove', intent: 'danger', type: 'submit' });
|
|
566
|
+
const life = new Lifetime();
|
|
567
|
+
const run = async () => {
|
|
568
|
+
if (pending || state.disabled || state.pending || confirm.element.disabled || options.unavailable?.())
|
|
569
|
+
return;
|
|
570
|
+
const seq = generation.next();
|
|
571
|
+
pending = true;
|
|
572
|
+
update(state);
|
|
573
|
+
try {
|
|
574
|
+
const result = await options.onConfirm();
|
|
575
|
+
if (!generation.current(seq) || !dialog.element.open)
|
|
576
|
+
return;
|
|
577
|
+
if (result.ok)
|
|
578
|
+
dialog.close();
|
|
579
|
+
else
|
|
580
|
+
state = { ...state, error: result.error };
|
|
581
|
+
}
|
|
582
|
+
catch (reason) {
|
|
583
|
+
if (generation.current(seq))
|
|
584
|
+
state = { ...state, error: reason instanceof Error ? reason.message : String(reason) };
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
if (generation.current(seq)) {
|
|
588
|
+
pending = false;
|
|
589
|
+
update(state);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const actions = createActions({ children: [cancel.element, confirm.element] });
|
|
594
|
+
const form = element('form', 'pui-confirm', message, hint, error.element, actions.element);
|
|
595
|
+
if (options.ids?.form)
|
|
596
|
+
form.id = options.ids.form;
|
|
597
|
+
if (options.ids?.error)
|
|
598
|
+
error.element.id = options.ids.error;
|
|
599
|
+
if (options.ids?.cancel)
|
|
600
|
+
cancel.element.id = options.ids.cancel;
|
|
601
|
+
const dialog = createDialog({ ...options, content: form,
|
|
602
|
+
onClose() { generation.invalidate(); pending = false; update(state); options.onClose?.(); } });
|
|
603
|
+
if (options.ids?.title) {
|
|
604
|
+
dialog.element.querySelector('h2').id = options.ids.title;
|
|
605
|
+
dialog.element.setAttribute('aria-labelledby', options.ids.title);
|
|
606
|
+
}
|
|
607
|
+
let currentMessage;
|
|
608
|
+
const update = (next) => {
|
|
609
|
+
state = next;
|
|
610
|
+
dialog.update(next);
|
|
611
|
+
if (next.message !== currentMessage) {
|
|
612
|
+
currentMessage = next.message;
|
|
613
|
+
if (next.message instanceof Node)
|
|
614
|
+
message.replaceChildren(next.message);
|
|
615
|
+
else
|
|
616
|
+
setText(message, next.message);
|
|
617
|
+
}
|
|
618
|
+
setText(hint, next.hint);
|
|
619
|
+
hint.hidden = !next.hint;
|
|
620
|
+
error.update({ message: next.error ?? '', tone: 'error' });
|
|
621
|
+
confirm.update({ label: next.confirmLabel ?? 'Remove', intent: 'danger', disabled: next.disabled, pending: !!(next.pending || pending) });
|
|
622
|
+
};
|
|
623
|
+
life.listen(form, 'submit', (event => { event.preventDefault(); void run(); }));
|
|
624
|
+
update(options);
|
|
625
|
+
return { element: dialog.element, confirmButton: confirm.element, cancelButton: cancel.element, update, open(next) { generation.invalidate(); pending = false; update({ ...(next ?? state), error: '' }); dialog.open(); cancel.element.focus(); },
|
|
626
|
+
close() { generation.invalidate(); dialog.close(); }, destroy() { generation.destroy(); life.destroy(); dialog.destroy(); confirm.destroy(); cancel.destroy(); } };
|
|
627
|
+
}
|
|
628
|
+
export function createPopover(options) {
|
|
629
|
+
let open = false;
|
|
630
|
+
let state = options;
|
|
631
|
+
const trigger = createButton({ label: options.label, onAction() { if (open)
|
|
632
|
+
close();
|
|
633
|
+
else
|
|
634
|
+
show(); } });
|
|
635
|
+
const panel = element('div', 'pui-popover-panel', options.content);
|
|
636
|
+
panel.hidden = true;
|
|
637
|
+
panel.id = uniqueId('pui-popover');
|
|
638
|
+
panel.setAttribute('role', 'region');
|
|
639
|
+
panel.setAttribute('aria-label', options.label);
|
|
640
|
+
trigger.element.setAttribute('aria-controls', panel.id);
|
|
641
|
+
trigger.element.setAttribute('aria-expanded', 'false');
|
|
642
|
+
const root = element('div', 'pui-popover', trigger.element, panel);
|
|
643
|
+
const life = new Lifetime();
|
|
644
|
+
function close(restoreFocus = false) {
|
|
645
|
+
if (!open)
|
|
646
|
+
return;
|
|
647
|
+
open = false;
|
|
648
|
+
panel.hidden = true;
|
|
649
|
+
trigger.element.setAttribute('aria-expanded', 'false');
|
|
650
|
+
options.onToggle?.(false);
|
|
651
|
+
if (restoreFocus && trigger.element.isConnected)
|
|
652
|
+
trigger.element.focus();
|
|
653
|
+
}
|
|
654
|
+
function show() {
|
|
655
|
+
if (state.disabled || open)
|
|
656
|
+
return;
|
|
657
|
+
open = true;
|
|
658
|
+
panel.hidden = false;
|
|
659
|
+
trigger.element.setAttribute('aria-expanded', 'true');
|
|
660
|
+
options.onToggle?.(true);
|
|
661
|
+
}
|
|
662
|
+
life.listen(document, 'pointerdown', (event => { if (open && event.target instanceof Node && !root.contains(event.target))
|
|
663
|
+
close(); }));
|
|
664
|
+
life.listen(root, 'keydown', (event => { if (open && event.key === 'Escape') {
|
|
665
|
+
event.preventDefault();
|
|
666
|
+
event.stopPropagation();
|
|
667
|
+
close(true);
|
|
668
|
+
} }));
|
|
669
|
+
life.listen(root, 'focusout', (() => { queueMicrotask(() => { if (open && !root.contains(document.activeElement))
|
|
670
|
+
close(); }); }));
|
|
671
|
+
const update = (next) => { state = next; trigger.update(next); panel.setAttribute('aria-label', next.label); if (next.disabled)
|
|
672
|
+
close(); };
|
|
673
|
+
update(options);
|
|
674
|
+
return { element: root, trigger: trigger.element, panel, update, open: show, close, isOpen: () => open,
|
|
675
|
+
destroy() { close(); life.destroy(); trigger.destroy(); } };
|
|
676
|
+
}
|
|
677
|
+
export function createMenu(options) {
|
|
678
|
+
const content = element('div', 'pui-menu');
|
|
679
|
+
content.setAttribute('role', 'menu');
|
|
680
|
+
const blocks = new Map();
|
|
681
|
+
let items = options.items;
|
|
682
|
+
const popover = createPopover({ label: options.label, content, disabled: options.disabled });
|
|
683
|
+
popover.trigger.setAttribute('aria-haspopup', 'menu');
|
|
684
|
+
const life = new Lifetime();
|
|
685
|
+
const update = (state) => {
|
|
686
|
+
items = state.items;
|
|
687
|
+
popover.update(state);
|
|
688
|
+
const keep = new Set(state.items.map(item => item.id));
|
|
689
|
+
for (const [id, block] of blocks)
|
|
690
|
+
if (!keep.has(id)) {
|
|
691
|
+
block.destroy();
|
|
692
|
+
block.element.remove();
|
|
693
|
+
blocks.delete(id);
|
|
694
|
+
}
|
|
695
|
+
for (const item of state.items) {
|
|
696
|
+
let block = blocks.get(item.id);
|
|
697
|
+
if (!block) {
|
|
698
|
+
block = createButton({ ...item, onAction: async () => {
|
|
699
|
+
const current = items.find(candidate => candidate.id === item.id);
|
|
700
|
+
if (!current || current.disabled)
|
|
701
|
+
return;
|
|
702
|
+
const result = await current.onAction();
|
|
703
|
+
if (!result || result.ok)
|
|
704
|
+
popover.close();
|
|
705
|
+
return result;
|
|
706
|
+
} });
|
|
707
|
+
blocks.set(item.id, block);
|
|
708
|
+
}
|
|
709
|
+
block.update(item);
|
|
710
|
+
block.element.setAttribute('role', item.checked == null ? 'menuitem' : 'menuitemcheckbox');
|
|
711
|
+
if (item.checked != null)
|
|
712
|
+
block.element.setAttribute('aria-checked', String(item.checked));
|
|
713
|
+
else
|
|
714
|
+
block.element.removeAttribute('aria-checked');
|
|
715
|
+
content.append(block.element);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
life.listen(popover.trigger, 'keydown', (event => {
|
|
719
|
+
const key = event.key;
|
|
720
|
+
if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
721
|
+
event.preventDefault();
|
|
722
|
+
popover.open();
|
|
723
|
+
const targets = focusableElements(content);
|
|
724
|
+
(key === 'ArrowDown' ? targets[0] : targets.at(-1))?.focus();
|
|
725
|
+
}
|
|
726
|
+
}));
|
|
727
|
+
life.listen(content, 'keydown', (event => {
|
|
728
|
+
const key = event.key;
|
|
729
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key))
|
|
730
|
+
return;
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
const targets = focusableElements(content);
|
|
733
|
+
const index = targets.indexOf(document.activeElement);
|
|
734
|
+
const next = key === 'Home' ? 0 : key === 'End' ? targets.length - 1 : (index + (key === 'ArrowUp' ? -1 : 1) + targets.length) % targets.length;
|
|
735
|
+
targets[next]?.focus();
|
|
736
|
+
}));
|
|
737
|
+
update(options);
|
|
738
|
+
return { ...popover, update, destroy() { life.destroy(); popover.destroy(); for (const block of blocks.values())
|
|
739
|
+
block.destroy(); } };
|
|
740
|
+
}
|
|
741
|
+
//# sourceMappingURL=primitives.js.map
|