@mdbase-dev/ui 0.1.0-beta.126 → 0.1.0-beta.148

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