pith-ui 0.1.0 → 0.1.4
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 +20 -1
- package/API.md +834 -272
- package/README.md +3 -1
- package/dist/composer.d.ts.map +1 -1
- package/dist/composer.js +48 -29
- package/dist/composer.js.map +1 -1
- package/dist/content/conversation-reader.d.ts.map +1 -1
- package/dist/content/conversation-reader.js +8 -1
- package/dist/content/conversation-reader.js.map +1 -1
- package/dist/content/conversation-view.d.ts +21 -4
- package/dist/content/conversation-view.d.ts.map +1 -1
- package/dist/content/conversation-view.js +72 -13
- package/dist/content/conversation-view.js.map +1 -1
- package/dist/content/image-viewer.d.ts +4 -2
- package/dist/content/image-viewer.d.ts.map +1 -1
- package/dist/content/image-viewer.js +36 -15
- package/dist/content/image-viewer.js.map +1 -1
- package/dist/content/paged-preview.d.ts +1 -1
- package/dist/content/paged-preview.d.ts.map +1 -1
- package/dist/content/paged-preview.js +10 -5
- package/dist/content/paged-preview.js.map +1 -1
- package/dist/content/views.d.ts +3 -0
- package/dist/content/views.d.ts.map +1 -1
- package/dist/content/views.js +110 -41
- package/dist/content/views.js.map +1 -1
- package/dist/content/welcome.d.ts.map +1 -1
- package/dist/content/welcome.js +19 -7
- package/dist/content/welcome.js.map +1 -1
- package/dist/dom.js +1 -1
- package/dist/dom.js.map +1 -1
- package/dist/files/panels.d.ts +8 -1
- package/dist/files/panels.d.ts.map +1 -1
- package/dist/files/panels.js +235 -120
- package/dist/files/panels.js.map +1 -1
- package/dist/files/systems.d.ts.map +1 -1
- package/dist/files/systems.js +81 -91
- package/dist/files/systems.js.map +1 -1
- package/dist/forms/actions.d.ts +28 -7
- package/dist/forms/actions.d.ts.map +1 -1
- package/dist/forms/actions.js +59 -11
- package/dist/forms/actions.js.map +1 -1
- package/dist/forms/custom-connection.d.ts +2 -1
- package/dist/forms/custom-connection.d.ts.map +1 -1
- package/dist/forms/custom-connection.js +36 -28
- package/dist/forms/custom-connection.js.map +1 -1
- package/dist/forms/dialog-frame.d.ts +2 -0
- package/dist/forms/dialog-frame.d.ts.map +1 -1
- package/dist/forms/dialog-frame.js +46 -26
- package/dist/forms/dialog-frame.js.map +1 -1
- package/dist/forms/history.d.ts +1 -1
- package/dist/forms/history.d.ts.map +1 -1
- package/dist/forms/history.js +78 -15
- package/dist/forms/history.js.map +1 -1
- package/dist/forms/identity.d.ts +12 -0
- package/dist/forms/identity.d.ts.map +1 -1
- package/dist/forms/identity.js +16 -0
- package/dist/forms/identity.js.map +1 -1
- package/dist/forms/model-editor.d.ts +8 -3
- package/dist/forms/model-editor.d.ts.map +1 -1
- package/dist/forms/model-editor.js +73 -62
- package/dist/forms/model-editor.js.map +1 -1
- package/dist/forms/panels.d.ts +103 -7
- package/dist/forms/panels.d.ts.map +1 -1
- package/dist/forms/panels.js +183 -77
- package/dist/forms/panels.js.map +1 -1
- package/dist/forms/provider-login.d.ts.map +1 -1
- package/dist/forms/provider-login.js +25 -39
- package/dist/forms/provider-login.js.map +1 -1
- package/dist/forms/resource-editor.d.ts +9 -5
- package/dist/forms/resource-editor.d.ts.map +1 -1
- package/dist/forms/resource-editor.js +20 -9
- package/dist/forms/resource-editor.js.map +1 -1
- package/dist/foundations.d.ts +95 -0
- package/dist/foundations.d.ts.map +1 -0
- package/dist/foundations.js +299 -0
- package/dist/foundations.js.map +1 -0
- package/dist/navigation/context-menu.d.ts +8 -1
- package/dist/navigation/context-menu.d.ts.map +1 -1
- package/dist/navigation/context-menu.js +67 -50
- package/dist/navigation/context-menu.js.map +1 -1
- package/dist/navigation/navigation.d.ts +55 -7
- package/dist/navigation/navigation.d.ts.map +1 -1
- package/dist/navigation/navigation.js +167 -18
- package/dist/navigation/navigation.js.map +1 -1
- package/dist/operation.d.ts +12 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +49 -0
- package/dist/operation.js.map +1 -0
- package/dist/primitives.d.ts +11 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +164 -301
- package/dist/primitives.js.map +1 -1
- package/dist/reconcile.d.ts +11 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +36 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/select.d.ts +14 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +304 -0
- package/dist/select.js.map +1 -0
- package/dist/styles.css +452 -382
- package/dist/surface.d.ts +60 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +344 -0
- package/dist/surface.js.map +1 -0
- package/dist/tasks/approval.d.ts.map +1 -1
- package/dist/tasks/approval.js +86 -45
- package/dist/tasks/approval.js.map +1 -1
- package/dist/tasks/permission-element.d.ts +3 -0
- package/dist/tasks/permission-element.d.ts.map +1 -0
- package/dist/tasks/permission-element.js +10 -0
- package/dist/tasks/permission-element.js.map +1 -0
- package/dist/tasks/permission.d.ts +5 -3
- package/dist/tasks/permission.d.ts.map +1 -1
- package/dist/tasks/permission.js +47 -22
- package/dist/tasks/permission.js.map +1 -1
- package/dist/tasks/runtime.d.ts.map +1 -1
- package/dist/tasks/runtime.js +30 -13
- package/dist/tasks/runtime.js.map +1 -1
- package/dist/workspace.d.ts +58 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +202 -44
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
package/dist/primitives.js
CHANGED
|
@@ -1,64 +1,22 @@
|
|
|
1
|
-
import { Generation, Lifetime
|
|
2
|
-
import { element,
|
|
1
|
+
import { Generation, Lifetime } from './core.js';
|
|
2
|
+
import { element, iconElement, setText, uniqueId } from './dom.js';
|
|
3
|
+
import { adoptAction, adoptActionLayout, adoptFeedback, adoptNativeControl, reconcileSelectOptions } from './foundations.js';
|
|
4
|
+
import { installAnchoredSurface, installDialogLifecycle, installMenuNavigation } from './surface.js';
|
|
5
|
+
import { reconcileKeyed } from './reconcile.js';
|
|
6
|
+
import { createOperation } from './operation.js';
|
|
3
7
|
export function createButton(options) {
|
|
4
8
|
const node = element('button', 'pui-button');
|
|
5
9
|
node.type = options.type ?? 'button';
|
|
6
10
|
const caption = element('span');
|
|
7
11
|
node.append(caption);
|
|
8
|
-
const
|
|
9
|
-
const generation = new Generation();
|
|
10
|
-
let state = options;
|
|
11
|
-
let localPending = false;
|
|
12
|
-
let currentIcon = '';
|
|
13
|
-
let glyph;
|
|
12
|
+
const action = adoptAction(node, { caption, onAction: options.onAction, onError: options.onError });
|
|
14
13
|
const update = (next) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
}
|
|
14
|
+
action.update({ ...next, intent: next.intent ?? 'neutral', density: next.density ?? 'normal',
|
|
15
|
+
disabled: !!next.disabled, pending: !!next.pending, title: next.title ?? '', icon: next.icon ?? '',
|
|
16
|
+
appearance: next.appearance ?? 'standard' });
|
|
29
17
|
};
|
|
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
18
|
update(options);
|
|
61
|
-
return { element: node, update, destroy() {
|
|
19
|
+
return { element: node, update, destroy() { action.destroy(); } };
|
|
62
20
|
}
|
|
63
21
|
export function createIconButton(options) {
|
|
64
22
|
const block = createButton(options);
|
|
@@ -116,35 +74,21 @@ export function createKeyValue(options) {
|
|
|
116
74
|
const root = element('dl', 'pui-key-value');
|
|
117
75
|
const rows = new Map();
|
|
118
76
|
const update = (state) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
}
|
|
77
|
+
reconcileKeyed(root, rows, state.items, { key: item => item.key, element: row => row.node,
|
|
78
|
+
create() { const label = element('dt'), value = element('dd'); return { node: element('div', '', label, value), label, value }; },
|
|
79
|
+
update(row, item) { setText(row.label, item.label); setText(row.value, item.value); }, destroy() { },
|
|
80
|
+
});
|
|
137
81
|
};
|
|
138
82
|
update(options);
|
|
139
83
|
return { element: root, update, destroy() { rows.clear(); } };
|
|
140
84
|
}
|
|
141
85
|
export function createToolbar(options) {
|
|
142
86
|
const root = element('div', 'pui-toolbar');
|
|
143
|
-
root.setAttribute('role', 'group');
|
|
144
87
|
root.append(...(options.children ?? []));
|
|
145
|
-
const
|
|
88
|
+
const layout = adoptActionLayout(root, 'toolbar');
|
|
89
|
+
const update = (state) => { layout.update({ ...state, density: state.density ?? 'normal' }); };
|
|
146
90
|
update(options);
|
|
147
|
-
return { element: root, update, destroy() { } };
|
|
91
|
+
return { element: root, update, destroy() { layout.destroy(); } };
|
|
148
92
|
}
|
|
149
93
|
export function createScrollArea(options) {
|
|
150
94
|
const root = element('div', 'pui-scroll-area', options.content);
|
|
@@ -160,33 +104,19 @@ export function createField(options) {
|
|
|
160
104
|
const description = element('div', 'pui-field-description');
|
|
161
105
|
const error = element('div', 'pui-field-error');
|
|
162
106
|
const control = options.control;
|
|
163
|
-
if (!control.id)
|
|
164
|
-
control.id = uniqueId('pui-input');
|
|
165
|
-
label.htmlFor = control.id;
|
|
166
107
|
description.id = uniqueId('pui-description');
|
|
167
108
|
error.id = uniqueId('pui-error');
|
|
168
|
-
error.setAttribute('role', 'alert');
|
|
169
109
|
control.classList.add('pui-control');
|
|
170
110
|
root.append(label, control, description, error);
|
|
111
|
+
const field = adoptNativeControl(control, { label, description, error });
|
|
171
112
|
const update = (state) => {
|
|
172
|
-
|
|
113
|
+
field.update({ ...state, label: `${state.label}${state.required ? ' *' : ''}`,
|
|
114
|
+
description: state.description ?? '', error: state.error ?? '', disabled: !!state.disabled, required: !!state.required });
|
|
173
115
|
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
116
|
root.classList.toggle('pui-invalid', !!state.error);
|
|
187
117
|
};
|
|
188
118
|
update(options);
|
|
189
|
-
return { element: root, control, update, destroy() { } };
|
|
119
|
+
return { element: root, control, update, destroy() { field.destroy(); } };
|
|
190
120
|
}
|
|
191
121
|
function textControl(options, type) {
|
|
192
122
|
const control = type === 'textarea' ? element('textarea') : element('input');
|
|
@@ -201,22 +131,19 @@ function textControl(options, type) {
|
|
|
201
131
|
}
|
|
202
132
|
if (options.autocomplete)
|
|
203
133
|
control.setAttribute('autocomplete', options.autocomplete);
|
|
134
|
+
control.defaultValue = options.value ?? '';
|
|
204
135
|
control.value = options.value ?? '';
|
|
205
136
|
const field = createField({ ...options, control });
|
|
206
|
-
const
|
|
207
|
-
life.listen(control, 'input', (event => options.onInput?.(control.value, event)));
|
|
208
|
-
life.listen(control, 'change', (event => options.onChange?.(control.value, event)));
|
|
137
|
+
const input = adoptNativeControl(control, { onInput: options.onInput, onChange: options.onChange });
|
|
209
138
|
const update = (state) => {
|
|
210
139
|
field.update(state);
|
|
211
|
-
|
|
212
|
-
control.readOnly = !!state.readOnly;
|
|
140
|
+
input.update({ placeholder: state.placeholder ?? '', readOnly: !!state.readOnly });
|
|
213
141
|
control.classList.toggle('pui-compact', state.density === 'compact');
|
|
214
142
|
};
|
|
215
143
|
update(options);
|
|
216
|
-
return { element: field.element, control, update, getValue:
|
|
217
|
-
reset
|
|
218
|
-
|
|
219
|
-
destroy() { life.destroy(); field.destroy(); } };
|
|
144
|
+
return { element: field.element, control, update, getValue: input.getValue,
|
|
145
|
+
reset: input.reset, focus: input.focus,
|
|
146
|
+
destroy() { input.destroy(); field.destroy(); } };
|
|
220
147
|
}
|
|
221
148
|
export const createTextInput = (options) => textControl(options, 'text');
|
|
222
149
|
export const createSearchInput = (options) => textControl(options, 'search');
|
|
@@ -238,6 +165,7 @@ export function createSecretInput(options) {
|
|
|
238
165
|
block.update = state => { update(state); toggle.update({ label: control.type === 'password' ? 'Show' : 'Hide', disabled: state.disabled }); };
|
|
239
166
|
const destroy = block.destroy;
|
|
240
167
|
block.destroy = () => { destroy(); toggle.destroy(); };
|
|
168
|
+
block.update(options);
|
|
241
169
|
return block;
|
|
242
170
|
}
|
|
243
171
|
export function createSelect(options) {
|
|
@@ -246,67 +174,79 @@ export function createSelect(options) {
|
|
|
246
174
|
if (options.size)
|
|
247
175
|
control.size = options.size;
|
|
248
176
|
const field = createField({ ...options, control });
|
|
249
|
-
|
|
177
|
+
let disposed = false;
|
|
178
|
+
const getValues = () => [...control.selectedOptions].map(option => option.value);
|
|
179
|
+
const input = adoptNativeControl(control, { onChange: () => { options.onChange?.(control.value); options.onValuesChange?.(getValues()); } });
|
|
180
|
+
const resetValues = (values) => {
|
|
181
|
+
if (disposed)
|
|
182
|
+
return;
|
|
183
|
+
if (!control.multiple) {
|
|
184
|
+
input.reset(values[0] ?? '');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const selected = new Set(values);
|
|
188
|
+
for (const option of control.options)
|
|
189
|
+
option.selected = selected.has(option.value);
|
|
190
|
+
input.update({});
|
|
191
|
+
};
|
|
250
192
|
let signature = '';
|
|
251
193
|
const update = (state) => {
|
|
194
|
+
if (disposed)
|
|
195
|
+
return;
|
|
252
196
|
field.update(state);
|
|
253
197
|
control.classList.toggle('pui-compact', state.density === 'compact');
|
|
254
198
|
const next = JSON.stringify(state.options);
|
|
255
199
|
if (next !== signature) {
|
|
256
200
|
signature = next;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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);
|
|
201
|
+
reconcileSelectOptions(control, state.options);
|
|
202
|
+
if (!control.multiple && control.selectedIndex < 0)
|
|
203
|
+
control.selectedIndex = [...control.options].findIndex(option => !option.disabled);
|
|
266
204
|
}
|
|
205
|
+
input.update({});
|
|
267
206
|
};
|
|
268
|
-
life.listen(control, 'change', (() => options.onChange?.(control.value)));
|
|
269
207
|
update(options);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
208
|
+
const initial = options.values ?? (options.value == null ? undefined : [options.value]);
|
|
209
|
+
if (initial) {
|
|
210
|
+
const selected = new Set(control.multiple ? initial : initial.slice(0, 1));
|
|
211
|
+
for (const option of control.options)
|
|
212
|
+
option.defaultSelected = selected.has(option.value);
|
|
213
|
+
resetValues(initial);
|
|
214
|
+
}
|
|
215
|
+
return { element: field.element, control, update, getValue: input.getValue, getValues,
|
|
216
|
+
reset: input.reset, resetValues, focus: input.focus, destroy() { if (disposed)
|
|
217
|
+
return; disposed = true; input.destroy(); field.destroy(); } };
|
|
274
218
|
}
|
|
275
219
|
export function createCheckbox(options) {
|
|
276
220
|
const control = element('input');
|
|
277
221
|
control.type = 'checkbox';
|
|
222
|
+
control.defaultChecked = !!options.checked;
|
|
278
223
|
control.checked = !!options.checked;
|
|
279
224
|
const caption = element('span');
|
|
280
225
|
const description = element('small');
|
|
281
226
|
const root = element('label', 'pui-checkbox', control, element('span', '', caption, description));
|
|
282
|
-
const
|
|
227
|
+
const input = adoptNativeControl(control, { onChange: () => options.onChange?.(control.checked) });
|
|
283
228
|
const update = (state) => {
|
|
284
229
|
setText(caption, state.label);
|
|
285
230
|
setText(description, state.description);
|
|
286
231
|
description.hidden = !state.description;
|
|
287
|
-
|
|
232
|
+
input.update({ disabled: !!state.disabled });
|
|
288
233
|
};
|
|
289
|
-
life.listen(control, 'change', (() => options.onChange?.(control.checked)));
|
|
290
234
|
update(options);
|
|
291
|
-
return { element: root, control, update, getValue:
|
|
292
|
-
reset
|
|
235
|
+
return { element: root, control, update, getValue: input.getChecked,
|
|
236
|
+
reset: input.reset, focus: input.focus, destroy() { input.destroy(); } };
|
|
293
237
|
}
|
|
294
238
|
export function createNotice(state) {
|
|
295
239
|
const root = element('div', 'pui-notice');
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
root.hidden = !next.message;
|
|
300
|
-
setText(root, next.message);
|
|
301
|
-
};
|
|
302
|
-
update(state);
|
|
303
|
-
return { element: root, update, destroy() { } };
|
|
240
|
+
const notice = adoptFeedback(root, { presentation: 'notice' });
|
|
241
|
+
notice.update(state);
|
|
242
|
+
return notice;
|
|
304
243
|
}
|
|
305
244
|
export function createStatus(state) {
|
|
306
245
|
const root = element('span', 'pui-status');
|
|
307
|
-
const
|
|
246
|
+
const status = adoptFeedback(root, { presentation: 'status', autoHide: false });
|
|
247
|
+
const update = (next) => { status.update({ message: next.label, tone: next.tone }); };
|
|
308
248
|
update(state);
|
|
309
|
-
return { element: root, update, destroy() { } };
|
|
249
|
+
return { element: root, update, destroy() { status.destroy(); } };
|
|
310
250
|
}
|
|
311
251
|
export function createProgress(state) {
|
|
312
252
|
const control = element('progress');
|
|
@@ -336,9 +276,10 @@ export function createFormSection(options) {
|
|
|
336
276
|
export function createActions(options = {}) {
|
|
337
277
|
const root = element('div', 'pui-actions');
|
|
338
278
|
root.append(...(options.children ?? []));
|
|
339
|
-
const
|
|
279
|
+
const layout = adoptActionLayout(root, 'actions');
|
|
280
|
+
const update = (state) => { layout.update({ align: state.align ?? 'end' }); };
|
|
340
281
|
update(options);
|
|
341
|
-
return { element: root, update, destroy() { } };
|
|
282
|
+
return { element: root, update, destroy() { layout.destroy(); } };
|
|
342
283
|
}
|
|
343
284
|
export function createDisclosure(options) {
|
|
344
285
|
const root = element('details', 'pui-disclosure');
|
|
@@ -424,9 +365,7 @@ export function createDialog(options) {
|
|
|
424
365
|
root.replaceChildren(heading, body);
|
|
425
366
|
if (options.actions)
|
|
426
367
|
root.append(options.actions);
|
|
427
|
-
const life = new Lifetime();
|
|
428
368
|
let state = options;
|
|
429
|
-
let previousFocus = null;
|
|
430
369
|
let disposed = false;
|
|
431
370
|
const update = (next) => {
|
|
432
371
|
state = next;
|
|
@@ -441,39 +380,14 @@ export function createDialog(options) {
|
|
|
441
380
|
root.removeAttribute('aria-describedby');
|
|
442
381
|
close.update({ label: `Close ${next.title}`, icon: 'close', disabled: next.closeDisabled });
|
|
443
382
|
};
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
}));
|
|
383
|
+
const surface = installDialogLifecycle(root, { closeOnBackdrop: options.closeOnBackdrop ?? !!options.dialog,
|
|
384
|
+
canClose: () => !state.closeDisabled, onClose: options.onClose });
|
|
463
385
|
update(options);
|
|
464
386
|
return { element: root, body, update,
|
|
465
|
-
open() {
|
|
466
|
-
|
|
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(); },
|
|
387
|
+
open() { if (!disposed)
|
|
388
|
+
surface.open(); }, close() { surface.close(); },
|
|
474
389
|
destroy() { if (disposed)
|
|
475
|
-
return; disposed = true;
|
|
476
|
-
root.close(); life.destroy(); close.destroy(); root.remove(); } };
|
|
390
|
+
return; disposed = true; surface.close(); surface.destroy(); close.destroy(); root.remove(); } };
|
|
477
391
|
}
|
|
478
392
|
export function createTextEditDialog(options = {}) {
|
|
479
393
|
// The editor has one form-level error, so it does not need the general field's
|
|
@@ -483,10 +397,12 @@ export function createTextEditDialog(options = {}) {
|
|
|
483
397
|
control.id = options.ids?.control ?? uniqueId('pui-input');
|
|
484
398
|
const label = element('label', 'pui-field-label', options.label ?? 'Message');
|
|
485
399
|
label.htmlFor = control.id;
|
|
400
|
+
const input = adoptNativeControl(control, { label, onInput: value => options.onInput?.(value) });
|
|
486
401
|
const field = element('div', 'pui-field', label, control);
|
|
487
402
|
const note = element('p', 'pui-muted');
|
|
488
403
|
const error = createNotice({ message: '', tone: 'error' });
|
|
489
|
-
const cancel = createButton({ label: 'Cancel', onAction() {
|
|
404
|
+
const cancel = createButton({ label: 'Cancel', onAction() { if (!state.closeDisabled)
|
|
405
|
+
dialog.close(); } });
|
|
490
406
|
const submit = createButton({ label: options.submitLabel ?? 'Save', intent: 'primary', type: 'submit' });
|
|
491
407
|
const actions = createActions({ children: [cancel.element, submit.element] });
|
|
492
408
|
const form = element('form', 'pui-text-edit', field, note, error.element, actions.element);
|
|
@@ -499,11 +415,10 @@ export function createTextEditDialog(options = {}) {
|
|
|
499
415
|
error.element.id = ids.error;
|
|
500
416
|
if (ids?.cancel)
|
|
501
417
|
cancel.element.id = ids.cancel;
|
|
502
|
-
const generation = new Generation();
|
|
503
418
|
const life = new Lifetime();
|
|
504
419
|
let pending = false;
|
|
505
420
|
let state = { title: options.title ?? 'Edit message', ...options };
|
|
506
|
-
const dialog = createDialog({ ...state, dialog: options.dialog, content: form, onClose() {
|
|
421
|
+
const dialog = createDialog({ ...state, dialog: options.dialog, content: form, onClose() { operation.invalidate(); options.onClose?.(); } });
|
|
507
422
|
if (ids?.title) {
|
|
508
423
|
dialog.element.querySelector('h2').id = ids.title;
|
|
509
424
|
dialog.element.setAttribute('aria-labelledby', ids.title);
|
|
@@ -513,83 +428,50 @@ export function createTextEditDialog(options = {}) {
|
|
|
513
428
|
dialog.update(next);
|
|
514
429
|
setText(note, next.note);
|
|
515
430
|
note.hidden = !next.note;
|
|
431
|
+
cancel.update({ label: 'Cancel', disabled: !!next.closeDisabled });
|
|
516
432
|
error.update({ message: next.error ?? '', tone: 'error' });
|
|
517
433
|
submit.update({ label: next.submitLabel ?? options.submitLabel ?? 'Save', intent: 'primary', disabled: next.disabled, pending: !!(next.pending || pending) });
|
|
518
|
-
|
|
434
|
+
input.update({ disabled: !!next.inputDisabled });
|
|
519
435
|
};
|
|
520
|
-
|
|
436
|
+
const operation = createOperation({
|
|
437
|
+
unavailable: () => !!(state.pending || state.disabled || submit.element.disabled || !dialog.element.open),
|
|
438
|
+
onPending(value) { pending = value; update(state); },
|
|
439
|
+
onError(error) { if (dialog.element.open)
|
|
440
|
+
update({ ...state, error }); },
|
|
441
|
+
});
|
|
521
442
|
life.listen(form, 'submit', (event => {
|
|
522
443
|
event.preventDefault();
|
|
523
|
-
if (
|
|
444
|
+
if (!options.onSubmit)
|
|
524
445
|
return;
|
|
525
|
-
const seq = generation.next();
|
|
526
446
|
const value = control.value;
|
|
527
|
-
|
|
528
|
-
update(state);
|
|
529
|
-
void Promise.resolve().then(() => options.onSubmit(value)).then(result => {
|
|
530
|
-
if (!generation.current(seq) || !dialog.element.open)
|
|
531
|
-
return;
|
|
447
|
+
operation.run(() => options.onSubmit(value), result => { if (dialog.element.open) {
|
|
532
448
|
if (result.ok)
|
|
533
449
|
dialog.close();
|
|
534
|
-
else
|
|
535
|
-
|
|
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);
|
|
450
|
+
else
|
|
451
|
+
update({ ...state, error: result.error });
|
|
543
452
|
} });
|
|
544
453
|
}));
|
|
545
454
|
update(state);
|
|
546
455
|
return { element: dialog.element, control, submitButton: submit.element, update,
|
|
547
|
-
open(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
control.value = input.value;
|
|
456
|
+
open(target) {
|
|
457
|
+
operation.invalidate();
|
|
458
|
+
update({ ...state, error: '', title: target.title ?? options.title ?? 'Edit message', description: target.description ?? options.description, note: target.note ?? options.note });
|
|
459
|
+
input.reset(target.value);
|
|
552
460
|
dialog.open();
|
|
553
|
-
|
|
554
|
-
}, close() {
|
|
555
|
-
destroy() {
|
|
461
|
+
input.focus();
|
|
462
|
+
}, close() { operation.invalidate(); dialog.close(); }, getValue: input.getValue, focus: input.focus,
|
|
463
|
+
destroy() { operation.destroy(); life.destroy(); input.destroy(); dialog.destroy(); cancel.destroy(); submit.destroy(); } };
|
|
556
464
|
}
|
|
557
465
|
export function createConfirmDialog(options) {
|
|
558
466
|
const message = element('p', 'pui-modal-description');
|
|
559
467
|
const hint = element('p', 'pui-field-hint');
|
|
560
468
|
const error = createNotice({ message: '', tone: 'error' });
|
|
561
|
-
const generation = new Generation();
|
|
562
469
|
let state = options;
|
|
563
470
|
let pending = false;
|
|
564
|
-
const cancel = createButton({ label: 'Cancel', onAction() {
|
|
471
|
+
const cancel = createButton({ label: 'Cancel', onAction() { if (!state.closeDisabled)
|
|
472
|
+
dialog.close(); } });
|
|
565
473
|
const confirm = createButton({ label: options.confirmLabel ?? 'Remove', intent: 'danger', type: 'submit' });
|
|
566
474
|
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
475
|
const actions = createActions({ children: [cancel.element, confirm.element] });
|
|
594
476
|
const form = element('form', 'pui-confirm', message, hint, error.element, actions.element);
|
|
595
477
|
if (options.ids?.form)
|
|
@@ -599,7 +481,7 @@ export function createConfirmDialog(options) {
|
|
|
599
481
|
if (options.ids?.cancel)
|
|
600
482
|
cancel.element.id = options.ids.cancel;
|
|
601
483
|
const dialog = createDialog({ ...options, content: form,
|
|
602
|
-
onClose() {
|
|
484
|
+
onClose() { operation.invalidate(); options.onClose?.(); } });
|
|
603
485
|
if (options.ids?.title) {
|
|
604
486
|
dialog.element.querySelector('h2').id = options.ids.title;
|
|
605
487
|
dialog.element.setAttribute('aria-labelledby', options.ids.title);
|
|
@@ -608,6 +490,7 @@ export function createConfirmDialog(options) {
|
|
|
608
490
|
const update = (next) => {
|
|
609
491
|
state = next;
|
|
610
492
|
dialog.update(next);
|
|
493
|
+
cancel.update({ label: 'Cancel', disabled: !!next.closeDisabled });
|
|
611
494
|
if (next.message !== currentMessage) {
|
|
612
495
|
currentMessage = next.message;
|
|
613
496
|
if (next.message instanceof Node)
|
|
@@ -618,20 +501,30 @@ export function createConfirmDialog(options) {
|
|
|
618
501
|
setText(hint, next.hint);
|
|
619
502
|
hint.hidden = !next.hint;
|
|
620
503
|
error.update({ message: next.error ?? '', tone: 'error' });
|
|
621
|
-
confirm.update({ label: next.confirmLabel ?? 'Remove', intent: 'danger', disabled: next.disabled, pending: !!(next.pending || pending) });
|
|
504
|
+
confirm.update({ label: next.confirmLabel ?? 'Remove', intent: 'danger', disabled: !!(next.disabled || options.unavailable?.()), pending: !!(next.pending || pending) });
|
|
622
505
|
};
|
|
623
|
-
|
|
506
|
+
const operation = createOperation({
|
|
507
|
+
unavailable: () => !!(state.disabled || state.pending || confirm.element.disabled || options.unavailable?.() || !dialog.element.open),
|
|
508
|
+
onPending(value) { pending = value; update(state); },
|
|
509
|
+
onError(error) { if (dialog.element.open)
|
|
510
|
+
update({ ...state, error }); },
|
|
511
|
+
});
|
|
512
|
+
life.listen(form, 'submit', (event => { event.preventDefault(); operation.run(options.onConfirm, result => { if (dialog.element.open) {
|
|
513
|
+
if (result.ok)
|
|
514
|
+
dialog.close();
|
|
515
|
+
else
|
|
516
|
+
update({ ...state, error: result.error });
|
|
517
|
+
} }); }));
|
|
624
518
|
update(options);
|
|
625
|
-
return { element: dialog.element, confirmButton: confirm.element, cancelButton: cancel.element, update, open(next) {
|
|
626
|
-
close() {
|
|
519
|
+
return { element: dialog.element, confirmButton: confirm.element, cancelButton: cancel.element, update, open(next) { operation.invalidate(); update({ ...(next ?? state), error: '' }); dialog.open(); cancel.element.focus(); },
|
|
520
|
+
close() { operation.invalidate(); dialog.close(); }, refreshAvailability() { update(state); }, destroy() { operation.destroy(); life.destroy(); dialog.destroy(); confirm.destroy(); cancel.destroy(); } };
|
|
627
521
|
}
|
|
628
522
|
export function createPopover(options) {
|
|
629
|
-
let open = false;
|
|
630
523
|
let state = options;
|
|
631
|
-
const trigger = createButton({ label: options.label, onAction() { if (
|
|
632
|
-
close();
|
|
524
|
+
const trigger = createButton({ label: options.label, onAction() { if (surface.isOpen())
|
|
525
|
+
surface.close();
|
|
633
526
|
else
|
|
634
|
-
|
|
527
|
+
surface.open(); } });
|
|
635
528
|
const panel = element('div', 'pui-popover-panel', options.content);
|
|
636
529
|
panel.hidden = true;
|
|
637
530
|
panel.id = uniqueId('pui-popover');
|
|
@@ -640,102 +533,72 @@ export function createPopover(options) {
|
|
|
640
533
|
trigger.element.setAttribute('aria-controls', panel.id);
|
|
641
534
|
trigger.element.setAttribute('aria-expanded', 'false');
|
|
642
535
|
const root = element('div', 'pui-popover', trigger.element, panel);
|
|
643
|
-
const
|
|
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(); }); }));
|
|
536
|
+
const surface = installAnchoredSurface({ trigger: trigger.element, panel, canOpen: () => !state.disabled, onToggle: options.onToggle });
|
|
671
537
|
const update = (next) => { state = next; trigger.update(next); panel.setAttribute('aria-label', next.label); if (next.disabled)
|
|
672
|
-
close(); };
|
|
538
|
+
surface.close(); };
|
|
673
539
|
update(options);
|
|
674
|
-
return { element: root, trigger: trigger.element, panel, update, open:
|
|
675
|
-
destroy() {
|
|
540
|
+
return { element: root, trigger: trigger.element, panel, update, open: surface.open, close: surface.close, isOpen: surface.isOpen,
|
|
541
|
+
destroy() { surface.destroy(); trigger.destroy(); } };
|
|
676
542
|
}
|
|
677
543
|
export function createMenu(options) {
|
|
678
544
|
const content = element('div', 'pui-menu');
|
|
679
545
|
content.setAttribute('role', 'menu');
|
|
546
|
+
content.tabIndex = -1;
|
|
680
547
|
const blocks = new Map();
|
|
681
548
|
let items = options.items;
|
|
682
|
-
const
|
|
549
|
+
const session = new Generation();
|
|
550
|
+
const feedback = createNotice({ message: '', tone: 'error' });
|
|
551
|
+
const menuBody = element('div', '', content, feedback.element);
|
|
552
|
+
const popover = createPopover({ label: options.label, content: menuBody, disabled: options.disabled, onToggle() { session.invalidate(); feedback.update({ message: '', tone: 'error' }); } });
|
|
683
553
|
popover.trigger.setAttribute('aria-haspopup', 'menu');
|
|
684
554
|
const life = new Lifetime();
|
|
685
555
|
const update = (state) => {
|
|
686
556
|
items = state.items;
|
|
687
557
|
popover.update(state);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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 () => {
|
|
558
|
+
reconcileKeyed(content, blocks, state.items, { key: item => item.id, element: block => block.element,
|
|
559
|
+
create(item) {
|
|
560
|
+
let sequence = 0;
|
|
561
|
+
return createButton({ ...item, onAction: async () => {
|
|
699
562
|
const current = items.find(candidate => candidate.id === item.id);
|
|
700
|
-
if (!current || current.disabled)
|
|
563
|
+
if (!current || current.disabled || !popover.isOpen())
|
|
701
564
|
return;
|
|
565
|
+
// Pending disables the action button. Keep focus inside its menu so
|
|
566
|
+
// focus-out dismissal cannot discard the eventual result feedback.
|
|
567
|
+
content.focus({ preventScroll: true });
|
|
568
|
+
sequence = session.next();
|
|
569
|
+
feedback.update({ message: '', tone: 'error' });
|
|
702
570
|
const result = await current.onAction();
|
|
703
|
-
if (!result || result.ok)
|
|
571
|
+
if (session.current(sequence) && blocks.has(item.id) && popover.isOpen() && (!result || result.ok))
|
|
704
572
|
popover.close();
|
|
705
573
|
return result;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
block.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
574
|
+
}, onError(message) { if (session.current(sequence) && popover.isOpen()) {
|
|
575
|
+
feedback.update({ message, tone: 'error' });
|
|
576
|
+
options.onError?.(message);
|
|
577
|
+
} } });
|
|
578
|
+
},
|
|
579
|
+
update(block, item) {
|
|
580
|
+
block.update(item);
|
|
581
|
+
block.element.setAttribute('role', item.checked == null ? 'menuitem' : 'menuitemcheckbox');
|
|
582
|
+
if (item.checked != null)
|
|
583
|
+
block.element.setAttribute('aria-checked', String(item.checked));
|
|
584
|
+
else
|
|
585
|
+
block.element.removeAttribute('aria-checked');
|
|
586
|
+
}, destroy: block => block.destroy(),
|
|
587
|
+
});
|
|
717
588
|
};
|
|
718
589
|
life.listen(popover.trigger, 'keydown', (event => {
|
|
719
590
|
const key = event.key;
|
|
720
591
|
if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
721
592
|
event.preventDefault();
|
|
722
593
|
popover.open();
|
|
723
|
-
|
|
724
|
-
(key === 'ArrowDown' ? targets[0] : targets.at(-1))?.focus();
|
|
594
|
+
navigation.focus(key === 'ArrowUp' ? 'last' : 'first');
|
|
725
595
|
}
|
|
726
596
|
}));
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
}));
|
|
597
|
+
const navigation = installMenuNavigation(content, { trigger: popover.trigger, isOpen: popover.isOpen,
|
|
598
|
+
onDismiss(restoreFocus) { popover.close(); if (restoreFocus)
|
|
599
|
+
popover.trigger.focus(); } });
|
|
737
600
|
update(options);
|
|
738
|
-
return { ...popover, update, destroy() { life.destroy(); popover.destroy(); for (const block of blocks.values())
|
|
739
|
-
block.destroy(); } };
|
|
601
|
+
return { ...popover, update, destroy() { session.destroy(); navigation.destroy(); life.destroy(); popover.destroy(); feedback.destroy(); for (const block of blocks.values())
|
|
602
|
+
block.destroy(); blocks.clear(); } };
|
|
740
603
|
}
|
|
741
604
|
//# sourceMappingURL=primitives.js.map
|