@mdbase-dev/ui 0.1.0-beta.125 → 0.1.0-beta.147

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.
Files changed (77) hide show
  1. package/README.md +159 -0
  2. package/css/brand.css +10 -10
  3. package/css/collections.css +978 -0
  4. package/css/controls.css +220 -43
  5. package/css/feedback.css +20 -20
  6. package/css/fonts.css +2 -3
  7. package/css/palette.css +10 -10
  8. package/css/screens.css +58 -16
  9. package/css/tokens.css +43 -3
  10. package/dist/AppSwitcher.js +187 -33
  11. package/dist/Checkbox.d.ts +14 -0
  12. package/dist/Checkbox.js +32 -0
  13. package/dist/CommandPalette.d.ts +4 -2
  14. package/dist/CommandPalette.js +275 -103
  15. package/dist/Dialog.js +72 -25
  16. package/dist/Feedback.js +763 -229
  17. package/dist/FeedbackMarkup.d.ts +1 -1
  18. package/dist/FeedbackMarkup.js +326 -125
  19. package/dist/FeedbackVerification.js +115 -68
  20. package/dist/SaveNotice.js +71 -28
  21. package/dist/Select.js +183 -135
  22. package/dist/SelectList.js +59 -14
  23. package/dist/ThemeSelect.js +55 -4
  24. package/dist/apps.d.ts +1 -0
  25. package/dist/apps.js +50 -29
  26. package/dist/brand.js +251 -68
  27. package/dist/codemirror.js +116 -87
  28. package/dist/collections/CollectionGate.d.ts +52 -0
  29. package/dist/collections/CollectionGate.js +456 -0
  30. package/dist/collections/CollectionSaveWarning.d.ts +10 -0
  31. package/dist/collections/CollectionSaveWarning.js +28 -0
  32. package/dist/collections/CollectionSwitcher.d.ts +26 -0
  33. package/dist/collections/CollectionSwitcher.js +579 -0
  34. package/dist/collections/SetupReview.d.ts +9 -0
  35. package/dist/collections/SetupReview.js +175 -0
  36. package/dist/collections/copy.d.ts +100 -0
  37. package/dist/collections/copy.js +937 -0
  38. package/dist/collections/dialogs.d.ts +27 -0
  39. package/dist/collections/dialogs.js +169 -0
  40. package/dist/collections/model.d.ts +146 -0
  41. package/dist/collections/model.js +337 -0
  42. package/dist/collections/mount.d.ts +18 -0
  43. package/dist/collections/mount.js +56 -0
  44. package/dist/collections/parts.d.ts +86 -0
  45. package/dist/collections/parts.js +439 -0
  46. package/dist/collections/testing.d.ts +19 -0
  47. package/dist/collections/testing.js +39 -0
  48. package/dist/collections/use-collection-ui.d.ts +140 -0
  49. package/dist/collections/use-collection-ui.js +236 -0
  50. package/dist/collections.d.ts +19 -0
  51. package/dist/collections.js +13 -0
  52. package/dist/feedback-data.d.ts +13 -12
  53. package/dist/feedback-data.js +365 -168
  54. package/dist/flock.d.ts +63 -0
  55. package/dist/flock.js +140 -0
  56. package/dist/i18n.d.ts +6 -0
  57. package/dist/i18n.js +71 -0
  58. package/dist/locales/de/messages.json +1 -0
  59. package/dist/locales/en/messages.json +1 -0
  60. package/dist/locales/es/messages.json +1 -0
  61. package/dist/locales/fr/messages.json +1 -0
  62. package/dist/locales/it/messages.json +1 -0
  63. package/dist/locales/ja/messages.json +1 -0
  64. package/dist/locales/ko/messages.json +1 -0
  65. package/dist/locales/pt/messages.json +1 -0
  66. package/dist/locales/ru/messages.json +1 -0
  67. package/dist/locales/zh/messages.json +1 -0
  68. package/dist/mark-activity.js +82 -75
  69. package/dist/permission-selection.js +27 -17
  70. package/dist/popover.js +138 -130
  71. package/dist/screens.d.ts +20 -3
  72. package/dist/screens.js +275 -6
  73. package/dist/select-model.js +34 -28
  74. package/dist/text-diff.d.ts +1 -0
  75. package/dist/text-diff.js +106 -74
  76. package/dist/theme.js +34 -40
  77. package/package.json +53 -8
@@ -0,0 +1,579 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useLayoutEffect, useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ import { anchoredPlacement } from "../popover.js";
4
+ import { RowDialogs, SignOutDialog } from "./dialogs.js";
5
+ import { AccountLine, ActionButton, CollectionMark, ItemCopy, ItemMore, NoticeBlock, PrivateMark, RowCopy, RowMore } from "./parts.js";
6
+ import { useCollectionUi } from "./use-collection-ui.js";
7
+ /** Collections worth a filter box; fewer fit at a glance. */
8
+ const FILTER_THRESHOLD = 7;
9
+ const NARROW = "(max-width: 639.98px)";
10
+ /**
11
+ * The in-app collection switcher: a trigger that opens the approved collections
12
+ * (one click switches, no Connect window), "Add a collection…", "Manage access"
13
+ * and the account. A popover on wide screens, a bottom sheet on narrow ones.
14
+ */
15
+ export function CollectionSwitcher({
16
+ variant = "full",
17
+ className,
18
+ icon,
19
+ ...options
20
+ }) {
21
+ const ui = useCollectionUi(options);
22
+ const [open, setOpen] = useState(false);
23
+ const [dialog, setDialog] = useState(null);
24
+ const trigger = useRef(null);
25
+ const panelId = useId();
26
+ const narrow = useNarrow();
27
+ const {
28
+ view,
29
+ copy,
30
+ say,
31
+ busy
32
+ } = ui;
33
+ const app = ui.appOpen?.item ?? null;
34
+ const current = app ? null : view.current;
35
+ const currentName = app?.name ?? current?.name ?? null;
36
+ const name = currentName ?? copy.switchCollectionNone;
37
+ const markOf = app ?? current;
38
+ const leading = icon === "mark" ? markOf ? _jsx(CollectionMark, {
39
+ row: markOf
40
+ }) : _jsx(CollectionsGlyph, {}) : icon;
41
+ const close = refocus => {
42
+ setOpen(false);
43
+ if (refocus) trigger.current?.focus();
44
+ };
45
+ const toDialog = next => {
46
+ setOpen(false);
47
+ setDialog(next);
48
+ };
49
+ // The popover that held the row is gone: a closed row dialog hands focus to the trigger.
50
+ const returnFocus = useRef(false);
51
+ useEffect(() => {
52
+ if (dialog || !returnFocus.current) return;
53
+ returnFocus.current = false;
54
+ const active = document.activeElement;
55
+ if (!active || active === document.body) trigger.current?.focus();
56
+ }, [dialog]);
57
+ const content = _jsx(SwitcherContent, {
58
+ ui: ui,
59
+ onDone: () => close(false),
60
+ onDialog: toDialog
61
+ });
62
+ return _jsxs("span", {
63
+ className: ["mdbase-collections", "mdbase-collections-switcher", className].filter(Boolean).join(" "),
64
+ "data-variant": variant,
65
+ children: [_jsxs("button", {
66
+ ref: trigger,
67
+ type: "button",
68
+ className: "mdbase-collections-trigger",
69
+ "aria-haspopup": "dialog",
70
+ "aria-expanded": open,
71
+ "aria-controls": open ? panelId : undefined,
72
+ "aria-label": currentName ? say("switchCollection", {
73
+ collection: currentName
74
+ }) : copy.switchCollectionNone,
75
+ title: name,
76
+ onClick: () => setOpen(value => !value),
77
+ children: [busy === "select" || busy === "add" || view.kind === "opening" ? _jsx("span", {
78
+ className: "mdbase-collections-spinner",
79
+ "aria-hidden": "true"
80
+ }) : leading !== undefined && leading !== null ? _jsx("span", {
81
+ className: "mdbase-collections-trigger-icon",
82
+ "aria-hidden": "true",
83
+ children: leading
84
+ }) : variant === "icon" ? _jsx(CollectionsGlyph, {}) : null, variant !== "icon" && _jsx("span", {
85
+ className: "mdbase-collections-trigger-name",
86
+ children: name
87
+ }), variant === "full" && current && _jsx("span", {
88
+ className: "mdbase-collections-trigger-role",
89
+ children: current.roleLabel
90
+ }), _jsx("svg", {
91
+ className: "mdbase-collections-chevron",
92
+ viewBox: "0 0 16 16",
93
+ "aria-hidden": "true",
94
+ children: _jsx("path", {
95
+ d: "m4 6 4 4 4-4"
96
+ })
97
+ })]
98
+ }), open && (narrow ? _jsx(SwitcherSheet, {
99
+ id: panelId,
100
+ ui: ui,
101
+ onClose: close,
102
+ children: content
103
+ }) : _jsx(SwitcherPopover, {
104
+ id: panelId,
105
+ ui: ui,
106
+ triggerRef: trigger,
107
+ onClose: close,
108
+ children: content
109
+ })), _jsx(RowDialogs, {
110
+ ui: ui,
111
+ dialog: dialog,
112
+ onClose: () => {
113
+ returnFocus.current = true;
114
+ setDialog(null);
115
+ }
116
+ }), _jsx(SignOutDialog, {
117
+ ui: ui
118
+ })]
119
+ });
120
+ }
121
+ /** The switcher's list and actions, also shown inline by `CollectionPanel`. */
122
+ export function SwitcherContent({
123
+ ui,
124
+ onDone,
125
+ onDialog
126
+ }) {
127
+ const {
128
+ view,
129
+ copy,
130
+ say,
131
+ busy,
132
+ actions
133
+ } = ui;
134
+ const [query, setQuery] = useState("");
135
+ const [focused, setFocused] = useState(null);
136
+ const lists = useRef(null);
137
+ const listLabel = useId();
138
+ const app = ui.appOpen;
139
+ const sections = (ui.options.sections ?? []).filter(section => section.items.length > 0 || section.empty);
140
+ const needle = query.trim().toLocaleLowerCase();
141
+ const matches = name => !needle || name.toLocaleLowerCase().includes(needle);
142
+ const total = view.collections.length + sections.reduce((sum, section) => sum + section.items.length, 0);
143
+ const signIn = view.signedIn ? null : _jsxs(_Fragment, {
144
+ children: [_jsx("p", {
145
+ className: "mdbase-collections-lead",
146
+ children: say("signedOutLead")
147
+ }), _jsx("div", {
148
+ className: "mdbase-collections-actions",
149
+ children: _jsx(ActionButton, {
150
+ tone: "primary",
151
+ progress: busy === "sign_in" ? copy.addingCollection : null,
152
+ inert: busy !== null,
153
+ onClick: () => {
154
+ actions.signIn();
155
+ onDone();
156
+ },
157
+ children: copy.signIn
158
+ })
159
+ })]
160
+ });
161
+ const appSections = sections.map(section => _jsx(SwitcherSection, {
162
+ ui: ui,
163
+ section: section,
164
+ matches: matches,
165
+ focused: focused,
166
+ onFocusItem: setFocused,
167
+ onDone: onDone,
168
+ onDialog: onDialog
169
+ }, section.id));
170
+ if (!view.signedIn) {
171
+ return _jsxs("div", {
172
+ className: "mdbase-collections-switcher-content",
173
+ children: [signIn, appSections.length > 0 && _jsx("div", {
174
+ ref: lists,
175
+ className: "mdbase-collections-switcher-lists",
176
+ onKeyDown: moveAmongRadios,
177
+ children: appSections
178
+ })]
179
+ });
180
+ }
181
+ const rows = view.collections.filter(row => matches(row.name));
182
+ const isCurrent = row => row.current && !app;
183
+ const sdkKey = row => `sdk:${row.id}`;
184
+ const rovingId = rows.some(row => sdkKey(row) === focused) ? focused : rows.find(isCurrent) ?? rows[0] ? sdkKey(rows.find(isCurrent) ?? rows[0]) : null;
185
+ const shown = rows.length + sections.reduce((sum, section) => sum + section.items.filter(item => matches(item.name)).length, 0);
186
+ return _jsxs("div", {
187
+ className: "mdbase-collections-switcher-content",
188
+ children: [_jsxs("div", {
189
+ className: "mdbase-collections-switcher-header",
190
+ children: [_jsx("span", {
191
+ className: "mdbase-collections-eyebrow",
192
+ children: copy.collectionLabel
193
+ }), app ? _jsxs("span", {
194
+ className: "mdbase-collections-switcher-current",
195
+ children: [_jsx("strong", {
196
+ children: app.item.name
197
+ }), app.item.detail && _jsx("span", {
198
+ className: "mdbase-collections-row-meta",
199
+ children: app.item.detail
200
+ })]
201
+ }) : view.current ? _jsxs("span", {
202
+ className: "mdbase-collections-switcher-current",
203
+ children: [_jsx("strong", {
204
+ children: view.current.name
205
+ }), view.current.private && _jsx(PrivateMark, {
206
+ ui: ui
207
+ }), _jsx("span", {
208
+ className: "mdbase-collections-chip",
209
+ children: view.current.roleLabel
210
+ }), view.current.status && _jsx("span", {
211
+ className: "mdbase-collections-row-meta",
212
+ children: view.current.status
213
+ })]
214
+ }) : _jsx("span", {
215
+ className: "mdbase-collections-row-meta",
216
+ children: view.heading
217
+ })]
218
+ }), total >= FILTER_THRESHOLD && _jsx("input", {
219
+ className: "mdbase-collections-field mdbase-collections-filter",
220
+ type: "search",
221
+ inputMode: "search",
222
+ "aria-label": copy.findCollection,
223
+ placeholder: copy.findCollection,
224
+ value: query,
225
+ onChange: event => setQuery(event.target.value),
226
+ onKeyDown: event => {
227
+ if (event.key === "ArrowDown") {
228
+ event.preventDefault();
229
+ lists.current?.querySelector('[role="radio"][tabindex="0"]')?.focus();
230
+ }
231
+ }
232
+ }), _jsxs("div", {
233
+ ref: lists,
234
+ className: "mdbase-collections-switcher-lists",
235
+ onKeyDown: moveAmongRadios,
236
+ children: [view.collections.length > 0 && _jsxs(_Fragment, {
237
+ children: [_jsx("span", {
238
+ id: listLabel,
239
+ className: "mdbase-collections-visually-hidden",
240
+ children: copy.yourCollections
241
+ }), _jsx("div", {
242
+ role: "radiogroup",
243
+ "aria-labelledby": listLabel,
244
+ className: "mdbase-collections-switcher-list",
245
+ children: rows.map(row => {
246
+ const detailId = row.statusDetail ? `${listLabel}-${row.id}` : undefined;
247
+ const checked = isCurrent(row);
248
+ return _jsxs("div", {
249
+ className: "mdbase-collections-item",
250
+ children: [_jsxs("button", {
251
+ type: "button",
252
+ role: "radio",
253
+ "aria-checked": checked,
254
+ "aria-disabled": !row.available || undefined,
255
+ "aria-describedby": detailId,
256
+ tabIndex: sdkKey(row) === rovingId ? 0 : -1,
257
+ className: "mdbase-collections-row",
258
+ onFocus: () => setFocused(sdkKey(row)),
259
+ onClick: () => {
260
+ if (!row.available || busy !== null) return;
261
+ actions.select(row.id);
262
+ onDone();
263
+ },
264
+ children: [_jsx(CollectionMark, {
265
+ row: row
266
+ }), _jsx(RowCopy, {
267
+ row: row,
268
+ ui: ui,
269
+ current: checked,
270
+ ...(detailId ? {
271
+ detailId
272
+ } : {})
273
+ }), checked && _jsx(CheckGlyph, {})]
274
+ }), _jsx(RowMore, {
275
+ row: row,
276
+ ui: ui,
277
+ onDialog: onDialog,
278
+ tabbable: sdkKey(row) === rovingId
279
+ })]
280
+ }, row.id);
281
+ })
282
+ })]
283
+ }), appSections, needle && shown === 0 && _jsx("p", {
284
+ className: "mdbase-collections-empty",
285
+ children: say("noMatches", {
286
+ query: query.trim()
287
+ })
288
+ })]
289
+ }), ui.error && _jsx(NoticeBlock, {
290
+ notice: ui.error,
291
+ ui: ui,
292
+ variant: "inline"
293
+ }), _jsxs("div", {
294
+ className: "mdbase-collections-switcher-actions",
295
+ children: [app && _jsx(ActionButton, {
296
+ tone: "quiet",
297
+ inert: busy !== null,
298
+ onClick: () => {
299
+ actions.chooseAnother();
300
+ onDone();
301
+ },
302
+ children: copy.chooseAnother
303
+ }), _jsxs(ActionButton, {
304
+ tone: "quiet",
305
+ className: "is-add",
306
+ inert: busy !== null && busy !== "add",
307
+ progress: busy === "add" ? copy.addingCollection : null,
308
+ onClick: () => {
309
+ actions.addCollection();
310
+ onDone();
311
+ },
312
+ children: [_jsx("svg", {
313
+ viewBox: "0 0 16 16",
314
+ "aria-hidden": "true",
315
+ children: _jsx("path", {
316
+ d: "M8 3v10M3 8h10"
317
+ })
318
+ }), copy.addCollection]
319
+ }), view.links && _jsx("a", {
320
+ className: "mdbase-collections-link",
321
+ href: view.links.manageAccess,
322
+ target: "_blank",
323
+ rel: "noopener noreferrer",
324
+ children: copy.manageAccess
325
+ })]
326
+ }), _jsx("div", {
327
+ className: "mdbase-collections-switcher-footer",
328
+ children: _jsx(AccountLine, {
329
+ ui: ui
330
+ })
331
+ })]
332
+ });
333
+ }
334
+ /** One app section as its own labelled radio group. */
335
+ function SwitcherSection({
336
+ ui,
337
+ section,
338
+ matches,
339
+ focused,
340
+ onFocusItem,
341
+ onDone,
342
+ onDialog
343
+ }) {
344
+ const label = useId();
345
+ const items = section.items.filter(item => matches(item.name));
346
+ if (section.items.length > 0 && items.length === 0) return null;
347
+ const key = id => `${section.id}:${id}`;
348
+ const roving = items.some(item => key(item.id) === focused) ? focused : key((items.find(item => item.current) ?? items[0])?.id ?? "");
349
+ return _jsxs("div", {
350
+ className: "mdbase-collections-switcher-section",
351
+ children: [_jsx("span", {
352
+ id: label,
353
+ className: "mdbase-collections-section-title",
354
+ children: section.title
355
+ }), items.length === 0 ? _jsx("p", {
356
+ className: "mdbase-collections-empty",
357
+ children: section.empty
358
+ }) : _jsx("div", {
359
+ role: "radiogroup",
360
+ "aria-labelledby": label,
361
+ className: "mdbase-collections-switcher-list",
362
+ children: items.map(item => _jsxs("div", {
363
+ className: "mdbase-collections-item",
364
+ children: [_jsxs("button", {
365
+ type: "button",
366
+ role: "radio",
367
+ "aria-checked": item.current === true,
368
+ "aria-disabled": item.disabled || undefined,
369
+ tabIndex: key(item.id) === roving ? 0 : -1,
370
+ className: "mdbase-collections-row",
371
+ onFocus: () => onFocusItem(key(item.id)),
372
+ onClick: () => {
373
+ if (item.disabled || ui.busy !== null) return;
374
+ ui.actions.selectItem(section, item);
375
+ onDone();
376
+ },
377
+ children: [_jsx(CollectionMark, {
378
+ row: item
379
+ }), _jsx(ItemCopy, {
380
+ item: item,
381
+ ui: ui
382
+ }), item.current && _jsx(CheckGlyph, {})]
383
+ }), _jsx(ItemMore, {
384
+ item: item,
385
+ ui: ui,
386
+ onDialog: onDialog,
387
+ tabbable: key(item.id) === roving
388
+ })]
389
+ }, item.id))
390
+ })]
391
+ });
392
+ }
393
+ /** Arrow keys within the focused row's radio group; Right and Left between a row and its menu button. */
394
+ function moveAmongRadios(event) {
395
+ const target = event.target;
396
+ if (event.key === "ArrowRight" && target.getAttribute("role") === "radio") {
397
+ const more = target.parentElement?.querySelector(".mdbase-collections-more");
398
+ if (more) {
399
+ more.focus();
400
+ event.preventDefault();
401
+ }
402
+ return;
403
+ }
404
+ if (event.key === "ArrowLeft" && target.classList.contains("mdbase-collections-more")) {
405
+ target.parentElement?.querySelector('[role="radio"]')?.focus();
406
+ event.preventDefault();
407
+ return;
408
+ }
409
+ const group = target.closest('[role="radiogroup"]');
410
+ if (!group || !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
411
+ const radios = [...group.querySelectorAll('[role="radio"]')];
412
+ if (radios.length === 0) return;
413
+ event.preventDefault();
414
+ const index = radios.indexOf(target);
415
+ const from = index >= 0 ? index : radios.findIndex(radio => radio.parentElement?.contains(target));
416
+ const next = event.key === "Home" ? 0 : event.key === "End" ? radios.length - 1 : (from + (event.key === "ArrowDown" ? 1 : -1) + radios.length) % radios.length;
417
+ radios[next]?.focus();
418
+ }
419
+ /** Where focus starts in the list: the open collection, else the first group's row. */
420
+ function firstRadio(root) {
421
+ return root.querySelector('[role="radio"][aria-checked="true"]') ?? root.querySelector('[role="radio"][tabindex="0"]');
422
+ }
423
+ function CheckGlyph() {
424
+ return _jsx("svg", {
425
+ className: "mdbase-collections-check",
426
+ viewBox: "0 0 16 16",
427
+ "aria-hidden": "true",
428
+ children: _jsx("path", {
429
+ d: "m3.5 8.5 3 3 6-7"
430
+ })
431
+ });
432
+ }
433
+ /** Wide screens: a non-modal dialog anchored to the trigger, in the top layer where supported. */
434
+ function SwitcherPopover({
435
+ id,
436
+ ui,
437
+ triggerRef,
438
+ onClose,
439
+ children
440
+ }) {
441
+ const ref = useRef(null);
442
+ const closeRef = useRef(onClose);
443
+ closeRef.current = onClose;
444
+ useLayoutEffect(() => {
445
+ const panel = ref.current;
446
+ const trigger = triggerRef.current;
447
+ if (!panel || !trigger) return undefined;
448
+ if (typeof panel.showPopover === "function") panel.showPopover();
449
+ const box = trigger.getBoundingClientRect();
450
+ const width = Math.min(340, window.innerWidth - 16);
451
+ panel.style.width = `${width}px`;
452
+ const place = anchoredPlacement(box, {
453
+ width,
454
+ height: panel.scrollHeight
455
+ }, {
456
+ width: window.innerWidth,
457
+ height: window.innerHeight
458
+ }, {
459
+ gap: 6
460
+ });
461
+ panel.style.top = `${place.top}px`;
462
+ panel.style.left = `${place.left}px`;
463
+ panel.style.maxHeight = `${place.maxHeight}px`;
464
+ (firstRadio(panel) ?? panel.querySelector("button") ?? panel).focus({
465
+ preventScroll: true
466
+ });
467
+ return () => {
468
+ if (typeof panel.hidePopover === "function" && panel.matches(":popover-open")) panel.hidePopover();
469
+ };
470
+ }, [triggerRef]);
471
+ useEffect(() => {
472
+ const outside = event => {
473
+ const target = event.target;
474
+ if (target && !ref.current?.contains(target) && !triggerRef.current?.contains(target)) closeRef.current(false);
475
+ };
476
+ const resize = () => closeRef.current(false);
477
+ document.addEventListener("pointerdown", outside, true);
478
+ window.addEventListener("resize", resize);
479
+ return () => {
480
+ document.removeEventListener("pointerdown", outside, true);
481
+ window.removeEventListener("resize", resize);
482
+ };
483
+ }, [triggerRef]);
484
+ return _jsx("div", {
485
+ ref: ref,
486
+ id: id,
487
+ className: "mdbase-collections-popover",
488
+ popover: "manual",
489
+ role: "dialog",
490
+ "aria-label": ui.copy.collectionsLabel,
491
+ tabIndex: -1,
492
+ onKeyDown: event => {
493
+ if (event.key !== "Escape") return;
494
+ event.preventDefault();
495
+ event.stopPropagation();
496
+ onClose(true);
497
+ },
498
+ onBlur: event => {
499
+ const next = event.relatedTarget;
500
+ if (next && !ref.current?.contains(next) && !triggerRef.current?.contains(next)) onClose(false);
501
+ },
502
+ children: children
503
+ });
504
+ }
505
+ /** Narrow screens: a modal bottom sheet on the native <dialog>. */
506
+ function SwitcherSheet({
507
+ id,
508
+ ui,
509
+ onClose,
510
+ children
511
+ }) {
512
+ const ref = useRef(null);
513
+ useLayoutEffect(() => {
514
+ const sheet = ref.current;
515
+ if (!sheet) return undefined;
516
+ if (!sheet.open) {
517
+ if (typeof sheet.showModal === "function") sheet.showModal();else sheet.setAttribute("open", "");
518
+ }
519
+ firstRadio(sheet)?.focus({
520
+ preventScroll: true
521
+ });
522
+ return () => {
523
+ if (sheet.open && typeof sheet.close === "function") sheet.close();
524
+ };
525
+ }, []);
526
+ return _jsx("dialog", {
527
+ ref: ref,
528
+ id: id,
529
+ className: "mdbase-collections mdbase-collections-sheet",
530
+ "aria-label": ui.copy.collectionsLabel,
531
+ onCancel: event => {
532
+ event.preventDefault();
533
+ onClose(true);
534
+ },
535
+ onClick: event => {
536
+ if (event.target === event.currentTarget) onClose(true);
537
+ },
538
+ children: _jsxs("div", {
539
+ className: "mdbase-collections-sheet-body",
540
+ children: [_jsxs("div", {
541
+ className: "mdbase-collections-sheet-header",
542
+ children: [_jsx("span", {
543
+ className: "mdbase-collections-sheet-title",
544
+ children: ui.copy.collectionsLabel
545
+ }), _jsx("button", {
546
+ type: "button",
547
+ className: "mdbase-collections-more",
548
+ "aria-label": ui.copy.close,
549
+ onClick: () => onClose(true),
550
+ children: _jsx("svg", {
551
+ viewBox: "0 0 16 16",
552
+ "aria-hidden": "true",
553
+ children: _jsx("path", {
554
+ d: "m4 4 8 8M12 4l-8 8"
555
+ })
556
+ })
557
+ })]
558
+ }), children]
559
+ })
560
+ });
561
+ }
562
+ function CollectionsGlyph() {
563
+ return _jsx("svg", {
564
+ className: "mdbase-collections-glyph",
565
+ viewBox: "0 0 16 16",
566
+ "aria-hidden": "true",
567
+ children: _jsx("path", {
568
+ d: "M2.5 4.5h4l1.2 1.5h5.8v6.5h-11z"
569
+ })
570
+ });
571
+ }
572
+ function subscribeNarrow(listener) {
573
+ const query = typeof window.matchMedia === "function" ? window.matchMedia(NARROW) : null;
574
+ query?.addEventListener("change", listener);
575
+ return () => query?.removeEventListener("change", listener);
576
+ }
577
+ function useNarrow() {
578
+ return useSyncExternalStore(subscribeNarrow, () => typeof window.matchMedia === "function" && window.matchMedia(NARROW).matches, () => false);
579
+ }
@@ -0,0 +1,9 @@
1
+ import { type JSX } from "react";
2
+ import type { CollectionUi } from "./use-collection-ui.js";
3
+ /**
4
+ * Reviewable setup: `setup.plan()` (read-only, so it starts at once), then
5
+ * `setup.apply(plan)` only from the person's click. Never configures silently.
6
+ */
7
+ export declare function SetupReview({ ui }: {
8
+ readonly ui: CollectionUi;
9
+ }): JSX.Element;