keystra 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +177 -2
  4. package/dist/chunk-2BLQUBG7.js +816 -0
  5. package/dist/chunk-77GGU3AX.js +135 -0
  6. package/dist/chunk-KDOJARJD.js +29 -0
  7. package/dist/chunk-LYYUOTBP.js +125 -0
  8. package/dist/chunk-S5YVXIMA.js +61 -0
  9. package/dist/chunk-SSIKGDD4.js +255 -0
  10. package/dist/chunk-U7YK6PPT.js +1729 -0
  11. package/dist/chunk-XFNYIPOW.js +253 -0
  12. package/dist/chunk-ZOJRXXQN.js +308 -0
  13. package/dist/core.cjs +2861 -0
  14. package/dist/core.d.cts +41 -0
  15. package/dist/core.d.ts +41 -0
  16. package/dist/core.js +7 -0
  17. package/dist/engine-COPfXXTp.d.ts +229 -0
  18. package/dist/engine-DcbkGRgz.d.cts +229 -0
  19. package/dist/focus.cjs +374 -0
  20. package/dist/focus.d.cts +74 -0
  21. package/dist/focus.d.ts +74 -0
  22. package/dist/focus.js +2 -0
  23. package/dist/index-IZkcWAXP.d.cts +395 -0
  24. package/dist/index-IZkcWAXP.d.ts +395 -0
  25. package/dist/index.cjs +3745 -0
  26. package/dist/index.d.cts +9 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.global.js +1 -0
  29. package/dist/index.js +9 -0
  30. package/dist/layout-C2-n8aUI.d.ts +17 -0
  31. package/dist/layout-CBzD-87D.d.cts +17 -0
  32. package/dist/navigation.cjs +293 -0
  33. package/dist/navigation.d.cts +33 -0
  34. package/dist/navigation.d.ts +33 -0
  35. package/dist/navigation.js +2 -0
  36. package/dist/patterns.cjs +3505 -0
  37. package/dist/patterns.d.cts +12 -0
  38. package/dist/patterns.d.ts +12 -0
  39. package/dist/patterns.js +7 -0
  40. package/dist/roving-focus-CT7Dm6di.d.ts +63 -0
  41. package/dist/roving-focus-ZQ0v7hLe.d.cts +63 -0
  42. package/dist/selection.cjs +266 -0
  43. package/dist/selection.d.cts +43 -0
  44. package/dist/selection.d.ts +43 -0
  45. package/dist/selection.js +2 -0
  46. package/dist/utils-CkveVeIJ.d.cts +18 -0
  47. package/dist/utils-JmPZTlys.d.ts +18 -0
  48. package/dist/utils.cjs +195 -0
  49. package/dist/utils.d.cts +3 -0
  50. package/dist/utils.d.ts +3 -0
  51. package/dist/utils.js +2 -0
  52. package/package.json +147 -4
@@ -0,0 +1,816 @@
1
+ import { NavigationController, MENU_KEYS, EventManager, KeyMatcher, normalizeKeyEvent, createKeystra, ROVING_KEYS, TABS_KEYS_VERTICAL, TABS_KEYS_HORIZONTAL, LIST_KEYS, TREE_KEYS, COMMAND_PALETTE_KEYS } from './chunk-U7YK6PPT.js';
2
+ import { createGridStrategy } from './chunk-XFNYIPOW.js';
3
+ import { setAttribute, resolveElement, focusElement, ensureId, contains, queryItems } from './chunk-77GGU3AX.js';
4
+
5
+ // src/patterns/shared.ts
6
+ var LANDMARKS = /* @__PURE__ */ new Set(["NAV", "MAIN", "ASIDE", "HEADER", "FOOTER", "FORM", "SECTION", "ARTICLE", "DIALOG"]);
7
+ function acceptsRole(element) {
8
+ return !element.hasAttribute("role") && !LANDMARKS.has(element.tagName);
9
+ }
10
+ function ensureRole(element, role, enabled) {
11
+ if (enabled && acceptsRole(element)) element.setAttribute("role", role);
12
+ }
13
+ function ensureItemRoles(container, items, containerRole, itemRole, enabled) {
14
+ if (!enabled || !container) return;
15
+ if (container.getAttribute("role") !== containerRole) return;
16
+ for (let i = 0; i < items.length; i++) ensureRole(items[i], itemRole, true);
17
+ }
18
+ function setAria(element, name, value, enabled) {
19
+ if (!enabled || !element) return;
20
+ setAttribute(element, name, value);
21
+ }
22
+
23
+ // src/patterns/tabs.ts
24
+ var TabsController = class extends NavigationController {
25
+ constructor(options) {
26
+ const orientation = options.orientation ?? "horizontal";
27
+ super(options, {
28
+ mode: options.mode ?? (orientation === "vertical" ? "vertical" : "horizontal"),
29
+ loop: options.loop ?? true,
30
+ defaultKeys: orientation === "vertical" ? TABS_KEYS_VERTICAL : TABS_KEYS_HORIZONTAL
31
+ });
32
+ this.panels = [];
33
+ this.activeValue = -1;
34
+ this.applying = false;
35
+ this.initializing = true;
36
+ this.orientationValue = orientation;
37
+ this.activationValue = options.activation ?? "automatic";
38
+ this.manageAria = options.aria !== false;
39
+ this.activeAttribute = options.activeAttribute ?? "aria-selected";
40
+ this.panelActiveAttribute = options.panelActiveAttribute ?? "hidden";
41
+ this.panelSelector = options.panels;
42
+ this.userOnActivate = options.onActivate;
43
+ if (this.manageAria && this.container) {
44
+ ensureRole(this.container, "tablist", true);
45
+ if (this.container.getAttribute("role") === "tablist") {
46
+ setAria(this.container, "aria-orientation", this.orientationValue, true);
47
+ }
48
+ }
49
+ this.on("focus", ({ index }) => {
50
+ if (this.activationValue === "automatic") this.activate(index);
51
+ });
52
+ this.on("action", ({ action, index }) => {
53
+ if (this.activationValue === "manual" && action === "select" && index >= 0) this.activate(index);
54
+ });
55
+ this.on("refresh", () => this.refreshPanels());
56
+ this.refreshPanels();
57
+ this.decorate();
58
+ const initial = this.detectInitialIndex();
59
+ this.applyActive(initial);
60
+ this.initializing = false;
61
+ }
62
+ get activeTab() {
63
+ return this.activeValue >= 0 ? this.items[this.activeValue] ?? null : null;
64
+ }
65
+ get activeIndex() {
66
+ return this.activeValue;
67
+ }
68
+ get orientation() {
69
+ return this.orientationValue;
70
+ }
71
+ get activation() {
72
+ return this.activationValue;
73
+ }
74
+ activate(indexOrElement) {
75
+ if (indexOrElement === null) {
76
+ this.applyActive(-1);
77
+ return;
78
+ }
79
+ const index = typeof indexOrElement === "number" ? indexOrElement : this.items.indexOf(indexOrElement);
80
+ if (index < 0 || index >= this.items.length) return;
81
+ this.applyActive(index, true);
82
+ }
83
+ refresh() {
84
+ const items = super.refresh();
85
+ this.refreshPanels();
86
+ this.decorate();
87
+ if (this.activeValue >= items.length) this.applyActive(items.length > 0 ? 0 : -1);
88
+ else this.applyActive(this.activeValue);
89
+ return items;
90
+ }
91
+ ownsAction(action) {
92
+ return action === "select";
93
+ }
94
+ refreshPanels() {
95
+ if (!this.panelSelector || !this.container) {
96
+ this.panels = [];
97
+ return;
98
+ }
99
+ let panels = queryItems(this.container, this.panelSelector);
100
+ if (panels.length === 0) {
101
+ const scope = this.container.parentElement;
102
+ if (scope && scope !== this.container.ownerDocument?.documentElement) {
103
+ panels = queryItems(scope, this.panelSelector);
104
+ }
105
+ }
106
+ this.panels = panels;
107
+ }
108
+ decorate() {
109
+ const items = this.items;
110
+ for (let i = 0; i < items.length; i++) {
111
+ const tab = items[i];
112
+ ensureRole(tab, "tab", this.manageAria);
113
+ if (!this.manageAria) continue;
114
+ const tabId = ensureId(tab, "keystra-tab");
115
+ const panel = this.panels[i];
116
+ if (!panel) continue;
117
+ const panelId = ensureId(panel, "keystra-panel");
118
+ ensureRole(panel, "tabpanel", true);
119
+ setAria(tab, "aria-controls", panelId, true);
120
+ setAria(panel, "aria-labelledby", tabId, true);
121
+ }
122
+ }
123
+ detectInitialIndex() {
124
+ const items = this.items;
125
+ for (let i = 0; i < items.length; i++) {
126
+ if (items[i].getAttribute(this.activeAttribute) === "true") return i;
127
+ }
128
+ return items.length > 0 ? 0 : -1;
129
+ }
130
+ applyActive(index, moveFocus = false) {
131
+ if (this.applying) return;
132
+ this.applying = true;
133
+ try {
134
+ this.applyActiveNow(index, moveFocus);
135
+ } finally {
136
+ this.applying = false;
137
+ }
138
+ }
139
+ applyActiveNow(index, moveFocus) {
140
+ const items = this.items;
141
+ const previous = this.activeValue;
142
+ this.activeValue = index;
143
+ const writeMarker = this.manageAria || this.activeAttribute !== "aria-selected";
144
+ const writePanels = this.manageAria;
145
+ for (let i = 0; i < items.length; i++) {
146
+ const tab = items[i];
147
+ const on = i === index;
148
+ if (writeMarker && tab.getAttribute(this.activeAttribute) !== (on ? "true" : "false")) {
149
+ tab.setAttribute(this.activeAttribute, on ? "true" : "false");
150
+ }
151
+ if (this.manageAria && this.activeAttribute !== "aria-selected") {
152
+ setAria(tab, "aria-selected", on ? "true" : "false", true);
153
+ }
154
+ if (!writePanels) continue;
155
+ const panel = this.panels[i];
156
+ if (!panel) continue;
157
+ if (this.panelActiveAttribute === "hidden") {
158
+ if (panel.hasAttribute("hidden") === on) panel.toggleAttribute("hidden", !on);
159
+ } else {
160
+ panel.setAttribute(this.panelActiveAttribute, on ? "true" : "false");
161
+ }
162
+ }
163
+ if (index >= 0) {
164
+ if (moveFocus && items[index] !== this.activeElement) this.focusIndex(index);
165
+ else if (!moveFocus) this.setActiveIndex(index, { focus: false });
166
+ }
167
+ if (previous !== index && index >= 0 && !this.initializing) {
168
+ const element = items[index];
169
+ if (element) this.userOnActivate?.(element, index);
170
+ }
171
+ }
172
+ };
173
+ function createTabs(options = {}) {
174
+ return new TabsController(options);
175
+ }
176
+
177
+ // src/patterns/menu.ts
178
+ var MenuController = class extends NavigationController {
179
+ constructor(options) {
180
+ super(
181
+ {
182
+ typeahead: true,
183
+ ...options,
184
+ enabled: false
185
+ },
186
+ {
187
+ mode: options.mode ?? "vertical",
188
+ loop: options.loop ?? true,
189
+ defaultKeys: MENU_KEYS
190
+ }
191
+ );
192
+ this.openValue = false;
193
+ this.unhidden = false;
194
+ this.closeOnSelect = options.closeOnSelect !== false;
195
+ this.returnFocusTarget = options.returnFocusTo;
196
+ this.manageAria = options.aria !== false;
197
+ this.toggleHiddenValue = options.toggleHidden !== false;
198
+ this.userOnOpen = options.onOpen;
199
+ this.userOnClose = options.onClose;
200
+ if (this.manageAria && this.container) ensureRole(this.container, "menu", true);
201
+ this.decorateItems();
202
+ this.on("select", () => {
203
+ if (this.closeOnSelect) this.close();
204
+ });
205
+ this.on("action", ({ action }) => {
206
+ if (action === "escape") this.close();
207
+ });
208
+ this.on("refresh", () => this.decorateItems());
209
+ }
210
+ decorateItems() {
211
+ ensureItemRoles(this.container, this.items, "menu", "menuitem", this.manageAria);
212
+ }
213
+ get isOpen() {
214
+ return this.openValue;
215
+ }
216
+ open(options = {}) {
217
+ if (this.openValue) return;
218
+ this.openValue = true;
219
+ this.enable();
220
+ this.rememberFocus();
221
+ if (this.toggleHiddenValue && this.container?.hasAttribute("hidden")) {
222
+ this.container.removeAttribute("hidden");
223
+ this.unhidden = true;
224
+ }
225
+ this.refresh();
226
+ if (this.manageAria && this.container) this.container.setAttribute("data-keystra-open", "true");
227
+ const target = options.focus ?? "first";
228
+ const items = this.items;
229
+ if (items.length === 0) {
230
+ this.userOnOpen?.(this);
231
+ return;
232
+ }
233
+ if (target === "current" && this.currentIndex >= 0) {
234
+ this.focusIndex(this.currentIndex);
235
+ } else if (target === "last") {
236
+ this.focusIndex(items.length - 1);
237
+ } else if (typeof target === "number") {
238
+ this.focusIndex(Math.max(0, Math.min(target, items.length - 1)));
239
+ } else {
240
+ this.focusIndex(0);
241
+ }
242
+ this.userOnOpen?.(this);
243
+ }
244
+ close(options = {}) {
245
+ if (!this.openValue) return;
246
+ this.openValue = false;
247
+ this.disable();
248
+ if (this.manageAria && this.container) this.container.removeAttribute("data-keystra-open");
249
+ if (options.restoreFocus !== false) {
250
+ const explicit = resolveElement(this.returnFocusTarget, this.container?.ownerDocument ?? null);
251
+ if (explicit) focusElement(explicit);
252
+ else this.restoreFocus();
253
+ }
254
+ if (this.unhidden) {
255
+ this.container?.setAttribute("hidden", "");
256
+ this.unhidden = false;
257
+ }
258
+ this.userOnClose?.(this);
259
+ }
260
+ toggle() {
261
+ if (this.openValue) this.close();
262
+ else this.open();
263
+ }
264
+ destroy() {
265
+ this.openValue = false;
266
+ super.destroy();
267
+ }
268
+ ownsAction(action) {
269
+ return action === "select" || action === "escape";
270
+ }
271
+ };
272
+ function createMenu(options = {}) {
273
+ return new MenuController(options);
274
+ }
275
+
276
+ // src/patterns/dropdown.ts
277
+ var DEFAULT_OPEN_KEYS = ["Enter", "Space", "ArrowDown", "ArrowUp"];
278
+ var DropdownController = class extends MenuController {
279
+ constructor(options) {
280
+ super({ typeahead: true, ...options });
281
+ this.extra = new EventManager();
282
+ const doc = this.container?.ownerDocument ?? (typeof document !== "undefined" ? document : null);
283
+ this.triggerElement = resolveElement(options.trigger, doc);
284
+ this.manageTriggerAria = options.aria !== false;
285
+ this.closeOnBlurValue = options.closeOnBlur === true;
286
+ this.closeOnOutsidePointerValue = options.closeOnOutsidePointer !== false;
287
+ const openKeys = options.openKeys === void 0 || options.openKeys === null ? DEFAULT_OPEN_KEYS : Array.isArray(options.openKeys) ? options.openKeys : [options.openKeys];
288
+ this.openMatcher = new KeyMatcher({ open: openKeys });
289
+ this.menuId = this.container ? ensureId(this.container, "keystra-menu") : null;
290
+ this.decorateTrigger();
291
+ this.wireTrigger();
292
+ this.on("action", ({ action, event }) => {
293
+ if (action !== "escape") return;
294
+ event?.preventDefault();
295
+ this.close();
296
+ });
297
+ }
298
+ get trigger() {
299
+ return this.triggerElement;
300
+ }
301
+ open(options = {}) {
302
+ const wasOpen = this.isOpen;
303
+ super.open(options);
304
+ if (!wasOpen && this.isOpen) this.syncTriggerState();
305
+ }
306
+ close(options = {}) {
307
+ const wasOpen = this.isOpen;
308
+ super.close({ restoreFocus: false });
309
+ if (wasOpen && options.restoreFocus !== false && this.triggerElement) {
310
+ focusElement(this.triggerElement);
311
+ }
312
+ if (wasOpen) this.syncTriggerState();
313
+ }
314
+ destroy() {
315
+ this.extra.dispose();
316
+ this.openMatcher.dispose();
317
+ if (this.triggerElement && this.manageTriggerAria) {
318
+ this.triggerElement.removeAttribute("aria-expanded");
319
+ if (this.menuId) this.triggerElement.removeAttribute("aria-controls");
320
+ }
321
+ super.destroy();
322
+ }
323
+ decorateTrigger() {
324
+ const trigger = this.triggerElement;
325
+ if (!trigger || !this.manageTriggerAria) return;
326
+ if (!trigger.hasAttribute("aria-haspopup")) trigger.setAttribute("aria-haspopup", "menu");
327
+ setAria(trigger, "aria-controls", this.menuId, true);
328
+ this.syncTriggerState();
329
+ }
330
+ syncTriggerState() {
331
+ if (!this.triggerElement || !this.manageTriggerAria) return;
332
+ this.triggerElement.setAttribute("aria-expanded", this.isOpen ? "true" : "false");
333
+ }
334
+ wireTrigger() {
335
+ const trigger = this.triggerElement;
336
+ if (!trigger) return;
337
+ this.extra.add(trigger, "click", () => {
338
+ this.toggle();
339
+ });
340
+ this.extra.add(trigger, "keydown", (rawEvent) => {
341
+ const event = rawEvent;
342
+ if (this.isOpen) {
343
+ if (event.key === "Tab") this.close({ restoreFocus: false });
344
+ return;
345
+ }
346
+ const normalized = normalizeKeyEvent(event);
347
+ const match = this.openMatcher.match(normalized, this);
348
+ if (!match?.complete) return;
349
+ event.preventDefault();
350
+ this.open({ focus: normalized.key === "ArrowUp" ? "last" : "first" });
351
+ });
352
+ if (typeof document !== "undefined") {
353
+ this.extra.add(document, "keydown", (rawEvent) => {
354
+ const event = rawEvent;
355
+ if (event.key !== "Tab" || event.defaultPrevented || !this.isOpen) return;
356
+ if (contains(this.container, event.target)) this.close();
357
+ });
358
+ }
359
+ if (this.closeOnOutsidePointerValue && typeof document !== "undefined") {
360
+ this.extra.add(document, "pointerdown", (rawEvent) => {
361
+ if (!this.isOpen) return;
362
+ const target = rawEvent.target;
363
+ if (contains(this.container, target) || contains(this.triggerElement, target)) return;
364
+ this.close({ restoreFocus: false });
365
+ });
366
+ }
367
+ if (this.closeOnBlurValue && typeof document !== "undefined") {
368
+ this.extra.add(document, "focusin", (rawEvent) => {
369
+ if (!this.isOpen) return;
370
+ const target = rawEvent.target;
371
+ if (contains(this.container, target) || contains(this.triggerElement, target)) return;
372
+ this.close({ restoreFocus: false });
373
+ });
374
+ }
375
+ }
376
+ };
377
+ function createDropdown(options = {}) {
378
+ return new DropdownController(options);
379
+ }
380
+
381
+ // src/patterns/list.ts
382
+ var ListController = class extends NavigationController {
383
+ constructor(options) {
384
+ super(options, {
385
+ mode: options.mode ?? "vertical",
386
+ loop: options.loop ?? false,
387
+ defaultKeys: LIST_KEYS
388
+ });
389
+ this.manageAria = options.aria !== false;
390
+ if (this.manageAria && this.container) {
391
+ ensureRole(this.container, "listbox", true);
392
+ if (this.container.getAttribute("role") === "listbox" && this.selectionManager) {
393
+ const multiple = this.selectionManager.mode === "multiple" || this.selectionManager.mode === "range";
394
+ setAria(this.container, "aria-multiselectable", multiple ? "true" : null, true);
395
+ }
396
+ }
397
+ this.decorateItems();
398
+ this.on("refresh", () => this.decorateItems());
399
+ }
400
+ decorateItems() {
401
+ ensureItemRoles(this.container, this.items, "listbox", "option", this.manageAria);
402
+ }
403
+ };
404
+ function createList(options = {}) {
405
+ const container = resolveElement(options.container);
406
+ const aria = options.aria !== false && container !== null && (container.getAttribute("role") === "listbox" || acceptsRole(container));
407
+ const selection = options.selection ?? { enabled: true, mode: "single", followFocus: true };
408
+ return new ListController({
409
+ typeahead: true,
410
+ ...options,
411
+ selection: typeof selection === "object" ? { aria, ...selection } : selection && { mode: "single", aria }
412
+ });
413
+ }
414
+
415
+ // src/patterns/grid.ts
416
+ var GridController = class extends NavigationController {
417
+ constructor(options) {
418
+ super(options, {
419
+ mode: "grid",
420
+ loop: options.loop ?? false,
421
+ columns: options.columns ?? 0,
422
+ strategy: createGridStrategy({
423
+ maintainColumn: options.maintainColumn,
424
+ rowHomeEnd: options.rowHomeEnd
425
+ })
426
+ });
427
+ this.manageAria = options.aria !== false;
428
+ this.decorateRoles();
429
+ this.on("refresh", () => this.decorateRoles());
430
+ }
431
+ decorateRoles() {
432
+ const container = this.container;
433
+ if (!this.manageAria || !container) return;
434
+ if (container.querySelector("[role='row']") === null) return;
435
+ ensureRole(container, "grid", true);
436
+ const items = this.items;
437
+ for (let i = 0; i < items.length; i++) {
438
+ const item = items[i];
439
+ if (item.parentElement?.getAttribute("role") === "row") ensureRole(item, "gridcell", true);
440
+ }
441
+ }
442
+ get columns() {
443
+ if (this.resolved.columns > 0) return this.resolved.columns;
444
+ const items = this.items;
445
+ if (items.length === 0) return 0;
446
+ let columns = 1;
447
+ const first = items[0];
448
+ const top = this.rectOf(first).top;
449
+ for (let i = 1; i < items.length; i++) {
450
+ if (Math.abs(this.rectOf(items[i]).top - top) < 2) columns++;
451
+ else break;
452
+ }
453
+ return columns;
454
+ }
455
+ get rows() {
456
+ const columns = this.columns;
457
+ if (columns <= 0) return 0;
458
+ return Math.ceil(this.items.length / columns);
459
+ }
460
+ get position() {
461
+ const index = this.activeIndex;
462
+ const columns = this.columns;
463
+ if (index < 0 || columns <= 0) return { row: -1, column: -1 };
464
+ return { row: Math.floor(index / columns), column: index % columns };
465
+ }
466
+ focusCell(row, column) {
467
+ const columns = this.columns;
468
+ if (columns <= 0) return null;
469
+ const index = row * columns + column;
470
+ if (index < 0 || index >= this.items.length) return null;
471
+ return this.focusIndex(index);
472
+ }
473
+ rectOf(element) {
474
+ return this.collection.layout.rect(element, this.collection.version);
475
+ }
476
+ };
477
+ function createGrid(options = {}) {
478
+ return new GridController(options);
479
+ }
480
+
481
+ // src/patterns/tree.ts
482
+ var DEFAULT_ITEM_SELECTOR = "[role='treeitem']";
483
+ var DEFAULT_GROUP_SELECTOR = "[role='group']";
484
+ var TreeController = class extends NavigationController {
485
+ constructor(options) {
486
+ const hiddenClass = options.hiddenClass;
487
+ super(
488
+ {
489
+ typeahead: true,
490
+ items: options.items ?? DEFAULT_ITEM_SELECTOR,
491
+ ...options,
492
+ filter: hiddenClass ? (element, index) => {
493
+ if (options.filter && !options.filter(element, index)) return false;
494
+ const group = element.parentElement?.closest(`.${hiddenClass}`);
495
+ return !group;
496
+ } : options.filter
497
+ },
498
+ {
499
+ mode: options.mode ?? "vertical",
500
+ loop: options.loop ?? false,
501
+ defaultKeys: TREE_KEYS
502
+ }
503
+ );
504
+ this.groupSelector = options.groupSelector ?? DEFAULT_GROUP_SELECTOR;
505
+ this.manageAria = options.aria !== false;
506
+ this.collapseBehavior = options.collapseBehavior ?? "attribute";
507
+ this.toggleHiddenValue = options.toggleHidden === true || this.collapseBehavior === "hidden";
508
+ this.hiddenClassValue = options.hiddenClass;
509
+ this.userOnExpand = options.onExpand;
510
+ this.userOnCollapse = options.onCollapse;
511
+ if (this.manageAria && this.container) ensureRole(this.container, "tree", true);
512
+ this.decorateItems();
513
+ this.on("action", ({ action, element }) => {
514
+ if (!element) return;
515
+ if (action === "expand") this.handleExpandKey(element);
516
+ else if (action === "collapse") this.handleCollapseKey(element);
517
+ });
518
+ }
519
+ decorateItems() {
520
+ ensureItemRoles(this.container, this.items, "tree", "treeitem", this.manageAria);
521
+ this.on("refresh", () => {
522
+ ensureItemRoles(this.container, this.items, "tree", "treeitem", this.manageAria);
523
+ });
524
+ }
525
+ isExpandable(element) {
526
+ if (element.hasAttribute("aria-expanded")) return true;
527
+ if (element.hasAttribute("data-expandable")) return true;
528
+ return element.querySelector(this.groupSelector) !== null;
529
+ }
530
+ isExpanded(element) {
531
+ const value = element.getAttribute("aria-expanded");
532
+ if (value !== null) return value === "true";
533
+ const group = element.querySelector(this.groupSelector);
534
+ if (!group) return false;
535
+ if (this.hiddenClassValue) return !group.classList.contains(this.hiddenClassValue);
536
+ return !group.hasAttribute("hidden");
537
+ }
538
+ expand(elementOrIndex) {
539
+ const element = this.elementOf(elementOrIndex);
540
+ if (!element || !this.isExpandable(element)) return;
541
+ if (this.isExpanded(element)) return;
542
+ this.setExpanded(element, true);
543
+ }
544
+ collapse(elementOrIndex) {
545
+ const element = this.elementOf(elementOrIndex);
546
+ if (!element || !this.isExpandable(element)) return;
547
+ if (!this.isExpanded(element)) return;
548
+ this.setExpanded(element, false);
549
+ }
550
+ toggleExpanded(elementOrIndex) {
551
+ const element = this.elementOf(elementOrIndex);
552
+ if (!element || !this.isExpandable(element)) return;
553
+ this.setExpanded(element, !this.isExpanded(element));
554
+ }
555
+ expandAll() {
556
+ const visited = /* @__PURE__ */ new Set();
557
+ for (let pending = true; pending; ) {
558
+ pending = false;
559
+ for (const element of this.items) {
560
+ if (visited.has(element)) continue;
561
+ visited.add(element);
562
+ if (!this.isExpandable(element) || this.isExpanded(element)) continue;
563
+ this.setExpanded(element, true);
564
+ pending = true;
565
+ }
566
+ }
567
+ }
568
+ collapseAll() {
569
+ for (const element of this.items) if (this.isExpandable(element)) this.setExpanded(element, false);
570
+ }
571
+ level(element) {
572
+ let depth = 0;
573
+ let node = element.parentElement;
574
+ while (node && node !== this.container) {
575
+ if (this.matchesGroup(node)) depth++;
576
+ node = node.parentElement;
577
+ }
578
+ return depth;
579
+ }
580
+ parent(element) {
581
+ const items = this.items;
582
+ let node = element.parentElement;
583
+ while (node && node !== this.container) {
584
+ if (items.indexOf(node) !== -1) return node;
585
+ node = node.parentElement;
586
+ }
587
+ return null;
588
+ }
589
+ elementOf(elementOrIndex) {
590
+ if (elementOrIndex === void 0) return this.activeElement;
591
+ if (typeof elementOrIndex === "number") return this.items[elementOrIndex] ?? null;
592
+ return elementOrIndex;
593
+ }
594
+ matchesGroup(node) {
595
+ if (typeof node.matches === "function") {
596
+ try {
597
+ return node.matches(this.groupSelector);
598
+ } catch {
599
+ return false;
600
+ }
601
+ }
602
+ return false;
603
+ }
604
+ setExpanded(element, expanded) {
605
+ if (this.collapseBehavior !== "none") {
606
+ setAria(element, "aria-expanded", expanded ? "true" : "false", this.manageAria);
607
+ }
608
+ const group = element.querySelector(this.groupSelector);
609
+ if (group) {
610
+ if (this.toggleHiddenValue) {
611
+ if (group.hasAttribute("hidden") === expanded) group.toggleAttribute("hidden", !expanded);
612
+ }
613
+ if (this.hiddenClassValue) group.classList.toggle(this.hiddenClassValue, !expanded);
614
+ }
615
+ this.refresh();
616
+ if (expanded) this.userOnExpand?.(element, this.items.indexOf(element));
617
+ else this.userOnCollapse?.(element, this.items.indexOf(element));
618
+ }
619
+ handleExpandKey(element) {
620
+ if (!this.isExpandable(element)) return;
621
+ if (!this.isExpanded(element)) {
622
+ this.setExpanded(element, true);
623
+ return;
624
+ }
625
+ const index = this.items.indexOf(element);
626
+ if (index !== -1 && index + 1 < this.items.length) this.focusIndex(index + 1);
627
+ }
628
+ handleCollapseKey(element) {
629
+ if (this.isExpandable(element) && this.isExpanded(element)) {
630
+ this.setExpanded(element, false);
631
+ return;
632
+ }
633
+ const parent = this.parent(element);
634
+ if (parent) this.focusElement(parent);
635
+ }
636
+ };
637
+ function createTree(options = {}) {
638
+ return new TreeController(options);
639
+ }
640
+
641
+ // src/patterns/command-palette.ts
642
+ var CommandPaletteController = class extends NavigationController {
643
+ constructor(options) {
644
+ const doc = typeof document !== "undefined" ? document : null;
645
+ const containerElement = resolveElement(options.container, doc);
646
+ const input = resolveElement(options.input, doc) ?? containerElement?.querySelector(
647
+ "input:not([type='hidden']), textarea"
648
+ ) ?? null;
649
+ super(
650
+ {
651
+ activeDescendant: input ?? true,
652
+ ...options
653
+ },
654
+ {
655
+ mode: options.mode ?? "vertical",
656
+ loop: options.loop ?? true,
657
+ defaultKeys: COMMAND_PALETTE_KEYS,
658
+ listenTarget: input ?? containerElement ?? doc
659
+ }
660
+ );
661
+ this.extra = new EventManager();
662
+ this.openValue = false;
663
+ this.listId = null;
664
+ this.listElement = null;
665
+ this.inputElement = input;
666
+ this.manageAria = options.aria !== false;
667
+ this.toggleHiddenValue = options.toggleHidden !== false;
668
+ this.userOnQuery = options.onQuery;
669
+ this.userOnSubmit = options.onSubmit;
670
+ this.userOnCancel = options.onCancel;
671
+ if (options.openKeys && typeof document !== "undefined") {
672
+ const bindings = Array.isArray(options.openKeys) ? options.openKeys : [options.openKeys];
673
+ this.openMatcher = new KeyMatcher({ open: bindings });
674
+ this.extra.add(document, "keydown", (rawEvent) => {
675
+ const event = rawEvent;
676
+ const match = this.openMatcher.match(normalizeKeyEvent(event), this);
677
+ if (!match?.complete) return;
678
+ event.preventDefault();
679
+ this.toggle();
680
+ });
681
+ } else {
682
+ this.openMatcher = null;
683
+ }
684
+ this.listElement = resolveElement(options.list, doc) ?? this.container;
685
+ if (this.manageAria && this.listElement) {
686
+ ensureRole(this.listElement, "listbox", true);
687
+ this.listId = ensureId(this.listElement, "keystra-palette");
688
+ }
689
+ if (this.manageAria && this.inputElement) {
690
+ ensureRole(this.inputElement, "combobox", true);
691
+ setAria(this.inputElement, "aria-controls", this.listId, true);
692
+ setAria(this.inputElement, "aria-expanded", "false", true);
693
+ if (!this.inputElement.hasAttribute("aria-autocomplete")) {
694
+ this.inputElement.setAttribute("aria-autocomplete", "list");
695
+ }
696
+ if (options.placeholder && !this.inputElement.hasAttribute("placeholder")) {
697
+ this.inputElement.setAttribute("placeholder", options.placeholder);
698
+ }
699
+ }
700
+ this.wireInput();
701
+ this.decorateItems();
702
+ this.on("refresh", () => this.decorateItems());
703
+ this.on("select", ({ element }) => {
704
+ this.userOnSubmit?.(element ?? null, this.query);
705
+ });
706
+ this.on("action", ({ action, event }) => {
707
+ if (action !== "escape") return;
708
+ event?.preventDefault();
709
+ this.userOnCancel?.();
710
+ this.close();
711
+ });
712
+ }
713
+ get isOpen() {
714
+ return this.openValue;
715
+ }
716
+ get query() {
717
+ return this.inputElement?.value ?? "";
718
+ }
719
+ get input() {
720
+ return this.inputElement;
721
+ }
722
+ open() {
723
+ if (this.openValue) return;
724
+ this.openValue = true;
725
+ this.enable();
726
+ this.rememberFocus();
727
+ if (this.container && this.toggleHiddenValue && this.container.hasAttribute("hidden")) {
728
+ this.container.removeAttribute("hidden");
729
+ }
730
+ if (this.inputElement) {
731
+ focusElement(this.inputElement);
732
+ if (typeof this.inputElement.select === "function") this.inputElement.select();
733
+ }
734
+ this.refresh();
735
+ this.setActiveIndex(0, { focus: false });
736
+ setAria(this.inputElement, "aria-expanded", "true", this.manageAria);
737
+ }
738
+ close(options = {}) {
739
+ if (!this.openValue) return;
740
+ this.openValue = false;
741
+ this.disable();
742
+ if (this.container && this.toggleHiddenValue && !this.container.hasAttribute("hidden")) {
743
+ this.container.setAttribute("hidden", "");
744
+ }
745
+ setAria(this.inputElement, "aria-expanded", "false", this.manageAria);
746
+ if (options.restoreFocus !== false) this.restoreFocus();
747
+ }
748
+ toggle() {
749
+ if (this.openValue) this.close();
750
+ else this.open();
751
+ }
752
+ setQuery(query) {
753
+ if (!this.inputElement) return;
754
+ if (this.inputElement.value === query) return;
755
+ this.inputElement.value = query;
756
+ this.userOnQuery?.(query);
757
+ }
758
+ destroy() {
759
+ this.extra.dispose();
760
+ this.openMatcher?.dispose();
761
+ if (this.inputElement && this.manageAria) {
762
+ this.inputElement.removeAttribute("aria-expanded");
763
+ if (this.listId) this.inputElement.removeAttribute("aria-controls");
764
+ }
765
+ super.destroy();
766
+ }
767
+ ownsAction(action) {
768
+ return action === "select" || action === "escape";
769
+ }
770
+ runAction(action, event, match) {
771
+ if (super.runAction(action, event, match)) return true;
772
+ if (action !== "select") return false;
773
+ this.userOnSubmit?.(null, this.query);
774
+ return true;
775
+ }
776
+ decorateItems() {
777
+ ensureItemRoles(this.listElement, this.items, "listbox", "option", this.manageAria);
778
+ }
779
+ wireInput() {
780
+ const input = this.inputElement;
781
+ if (!input) return;
782
+ this.extra.add(input, "input", () => {
783
+ this.userOnQuery?.(input.value);
784
+ this.refresh();
785
+ this.setActiveIndex(0, { focus: false });
786
+ });
787
+ }
788
+ };
789
+ function createCommandPalette(options = {}) {
790
+ return new CommandPaletteController(options);
791
+ }
792
+
793
+ // src/patterns/roving-focus.ts
794
+ function createRovingFocus(options = {}) {
795
+ const instance = createKeystra({
796
+ keys: options.mode === void 0 ? ROVING_KEYS : void 0,
797
+ loop: true,
798
+ roving: true,
799
+ ...options
800
+ });
801
+ const items = instance.items;
802
+ if (items.length === 0) return instance;
803
+ let initial = options.initialIndex ?? -1;
804
+ if (initial < 0) {
805
+ for (let i = 0; i < items.length; i++) {
806
+ if (items[i].getAttribute("tabindex") === "0") {
807
+ initial = i;
808
+ break;
809
+ }
810
+ }
811
+ }
812
+ if (initial > 0) instance.setActiveIndex(initial, { focus: false });
813
+ return instance;
814
+ }
815
+
816
+ export { DropdownController, MenuController, createCommandPalette, createDropdown, createGrid, createList, createMenu, createRovingFocus, createTabs, createTree, ensureRole, setAria };