@dogsbay/primitives 0.2.0-beta.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/dist/chunk-4CMGFJKB.js +95 -0
  2. package/dist/chunk-4CMGFJKB.js.map +1 -0
  3. package/dist/chunk-HN6LBLTZ.js +24 -0
  4. package/dist/chunk-HN6LBLTZ.js.map +1 -0
  5. package/dist/chunk-I6QJ4ATJ.js +34 -0
  6. package/dist/chunk-I6QJ4ATJ.js.map +1 -0
  7. package/dist/chunk-LN2MVY72.js +113 -0
  8. package/dist/chunk-LN2MVY72.js.map +1 -0
  9. package/dist/chunk-M46BYUQY.js +79 -0
  10. package/dist/chunk-M46BYUQY.js.map +1 -0
  11. package/dist/chunk-N2FDXX6C.js +35 -0
  12. package/dist/chunk-N2FDXX6C.js.map +1 -0
  13. package/dist/chunk-OKZYYVLM.js +48 -0
  14. package/dist/chunk-OKZYYVLM.js.map +1 -0
  15. package/dist/chunk-Q2HX4RL6.js +54 -0
  16. package/dist/chunk-Q2HX4RL6.js.map +1 -0
  17. package/dist/chunk-QFU7X4UD.js +39 -0
  18. package/dist/chunk-QFU7X4UD.js.map +1 -0
  19. package/dist/collapsible.d.ts +31 -0
  20. package/dist/collapsible.js +59 -0
  21. package/dist/collapsible.js.map +1 -0
  22. package/dist/composite.d.ts +37 -0
  23. package/dist/composite.js +7 -0
  24. package/dist/composite.js.map +1 -0
  25. package/dist/dismiss.d.ts +38 -0
  26. package/dist/dismiss.js +7 -0
  27. package/dist/dismiss.js.map +1 -0
  28. package/dist/floating.d.ts +29 -0
  29. package/dist/floating.js +7 -0
  30. package/dist/floating.js.map +1 -0
  31. package/dist/focus-trap.d.ts +26 -0
  32. package/dist/focus-trap.js +7 -0
  33. package/dist/focus-trap.js.map +1 -0
  34. package/dist/index.d.ts +10 -0
  35. package/dist/index.js +39 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/inert.d.ts +10 -0
  38. package/dist/inert.js +7 -0
  39. package/dist/inert.js.map +1 -0
  40. package/dist/merge-props.d.ts +13 -0
  41. package/dist/merge-props.js +7 -0
  42. package/dist/merge-props.js.map +1 -0
  43. package/dist/portal.d.ts +9 -0
  44. package/dist/portal.js +7 -0
  45. package/dist/portal.js.map +1 -0
  46. package/dist/scroll-lock.d.ts +18 -0
  47. package/dist/scroll-lock.js +7 -0
  48. package/dist/scroll-lock.js.map +1 -0
  49. package/dist/typeahead.d.ts +35 -0
  50. package/dist/typeahead.js +7 -0
  51. package/dist/typeahead.js.map +1 -0
  52. package/package.json +85 -0
@@ -0,0 +1,95 @@
1
+ // src/dismiss.ts
2
+ var dismissStack = [];
3
+ function globalEscapeHandler(e) {
4
+ if (e.key !== "Escape") return;
5
+ if (dismissStack.length === 0) return;
6
+ const top = dismissStack[dismissStack.length - 1];
7
+ if (top.escapePrevented) return;
8
+ e.stopPropagation();
9
+ top.opts.onDismiss("escape-key");
10
+ }
11
+ var globalEscapeRegistered = false;
12
+ function ensureGlobalEscapeListener() {
13
+ if (globalEscapeRegistered) return;
14
+ globalEscapeRegistered = true;
15
+ document.addEventListener("keydown", globalEscapeHandler);
16
+ }
17
+ var DismissController = class {
18
+ root;
19
+ /** @internal */
20
+ opts;
21
+ outsideHandler = null;
22
+ active = false;
23
+ frameId = null;
24
+ /** @internal */
25
+ escapePrevented = false;
26
+ constructor(root, opts) {
27
+ this.root = root;
28
+ this.opts = {
29
+ onDismiss: opts.onDismiss,
30
+ escapeKey: opts.escapeKey ?? true,
31
+ outsidePress: opts.outsidePress ?? true
32
+ };
33
+ }
34
+ activate() {
35
+ if (this.active) return;
36
+ this.active = true;
37
+ if (this.opts.escapeKey) {
38
+ ensureGlobalEscapeListener();
39
+ dismissStack.push(this);
40
+ }
41
+ if (this.opts.outsidePress) {
42
+ this.outsideHandler = (e) => {
43
+ if (dismissStack.length > 0 && dismissStack[dismissStack.length - 1] !== this) {
44
+ return;
45
+ }
46
+ const target = e.target;
47
+ if (this.root.contains(target)) return;
48
+ const controlsId = this.root.id;
49
+ if (controlsId) {
50
+ const trigger = document.querySelector(
51
+ `[aria-controls="${controlsId}"]`
52
+ );
53
+ if (trigger?.contains(target)) return;
54
+ }
55
+ const triggerId = this.root.getAttribute("aria-labelledby");
56
+ if (triggerId) {
57
+ const trigger = document.getElementById(triggerId);
58
+ if (trigger?.contains(target)) return;
59
+ }
60
+ if (typeof this.opts.outsidePress === "function") {
61
+ if (!this.opts.outsidePress(e)) return;
62
+ }
63
+ this.opts.onDismiss("outside-press");
64
+ };
65
+ this.frameId = requestAnimationFrame(() => {
66
+ this.frameId = null;
67
+ if (this.outsideHandler) {
68
+ document.addEventListener("pointerdown", this.outsideHandler);
69
+ }
70
+ });
71
+ }
72
+ }
73
+ deactivate() {
74
+ if (!this.active) return;
75
+ this.active = false;
76
+ const idx = dismissStack.indexOf(this);
77
+ if (idx !== -1) dismissStack.splice(idx, 1);
78
+ if (this.frameId !== null) {
79
+ cancelAnimationFrame(this.frameId);
80
+ this.frameId = null;
81
+ }
82
+ if (this.outsideHandler) {
83
+ document.removeEventListener("pointerdown", this.outsideHandler);
84
+ this.outsideHandler = null;
85
+ }
86
+ }
87
+ isActive() {
88
+ return this.active;
89
+ }
90
+ };
91
+
92
+ export {
93
+ DismissController
94
+ };
95
+ //# sourceMappingURL=chunk-4CMGFJKB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dismiss.ts"],"sourcesContent":["export type DismissReason = \"escape-key\" | \"outside-press\";\n\nexport interface DismissOptions {\n /** Called when the user dismisses the element. */\n onDismiss: (reason: DismissReason) => void;\n /** Whether pressing Escape dismisses. Defaults to true. */\n escapeKey?: boolean;\n /** Whether clicking outside dismisses. Can be a predicate. Defaults to true. */\n outsidePress?: boolean | ((event: PointerEvent) => boolean);\n}\n\n/**\n * Stack of active dismiss controllers. Only the topmost controller\n * handles Escape — this prevents nested dialogs from all closing\n * on a single Escape press.\n */\nconst dismissStack: DismissController[] = [];\n\nfunction globalEscapeHandler(e: KeyboardEvent) {\n if (e.key !== \"Escape\") return;\n if (dismissStack.length === 0) return;\n\n // Only the topmost controller handles Escape\n const top = dismissStack[dismissStack.length - 1];\n if (top.escapePrevented) return;\n\n e.stopPropagation();\n top.opts.onDismiss(\"escape-key\");\n}\n\n// Single global listener, registered once\nlet globalEscapeRegistered = false;\n\nfunction ensureGlobalEscapeListener() {\n if (globalEscapeRegistered) return;\n globalEscapeRegistered = true;\n document.addEventListener(\"keydown\", globalEscapeHandler);\n}\n\n/**\n * Handles dismissal of floating/overlay elements via Escape key\n * and outside pointer press.\n *\n * Uses a global stack so that only the topmost (most recently opened)\n * controller handles Escape. This prevents nested dialogs from all\n * closing on a single keypress.\n *\n * Delays the outside-press listener by one animation frame to prevent\n * the opening click from immediately triggering a dismiss.\n */\nexport class DismissController {\n private root: HTMLElement;\n /** @internal */ opts: {\n onDismiss: (reason: DismissReason) => void;\n escapeKey: boolean;\n outsidePress: boolean | ((event: PointerEvent) => boolean);\n };\n private outsideHandler: ((e: PointerEvent) => void) | null = null;\n private active = false;\n private frameId: number | null = null;\n /** @internal */ escapePrevented = false;\n\n constructor(root: HTMLElement, opts: DismissOptions) {\n this.root = root;\n this.opts = {\n onDismiss: opts.onDismiss,\n escapeKey: opts.escapeKey ?? true,\n outsidePress: opts.outsidePress ?? true,\n };\n }\n\n activate() {\n if (this.active) return;\n this.active = true;\n\n if (this.opts.escapeKey) {\n ensureGlobalEscapeListener();\n dismissStack.push(this);\n }\n\n if (this.opts.outsidePress) {\n this.outsideHandler = (e: PointerEvent) => {\n // Only the topmost controller handles outside press.\n // This prevents a nested dialog's close button click from\n // being seen as an \"outside press\" by the parent dialog.\n if (\n dismissStack.length > 0 &&\n dismissStack[dismissStack.length - 1] !== this\n ) {\n return;\n }\n\n const target = e.target as Node;\n if (this.root.contains(target)) return;\n\n // Don't dismiss if clicking the trigger that owns this element\n const controlsId = this.root.id;\n if (controlsId) {\n const trigger = document.querySelector(\n `[aria-controls=\"${controlsId}\"]`,\n );\n if (trigger?.contains(target)) return;\n }\n const triggerId = this.root.getAttribute(\"aria-labelledby\");\n if (triggerId) {\n const trigger = document.getElementById(triggerId);\n if (trigger?.contains(target)) return;\n }\n\n if (typeof this.opts.outsidePress === \"function\") {\n if (!this.opts.outsidePress(e)) return;\n }\n\n this.opts.onDismiss(\"outside-press\");\n };\n\n // Delay to avoid the opening click triggering immediate dismiss\n this.frameId = requestAnimationFrame(() => {\n this.frameId = null;\n if (this.outsideHandler) {\n document.addEventListener(\"pointerdown\", this.outsideHandler);\n }\n });\n }\n }\n\n deactivate() {\n if (!this.active) return;\n this.active = false;\n\n // Remove from stack\n const idx = dismissStack.indexOf(this);\n if (idx !== -1) dismissStack.splice(idx, 1);\n\n if (this.frameId !== null) {\n cancelAnimationFrame(this.frameId);\n this.frameId = null;\n }\n\n if (this.outsideHandler) {\n document.removeEventListener(\"pointerdown\", this.outsideHandler);\n this.outsideHandler = null;\n }\n }\n\n isActive() {\n return this.active;\n }\n}\n"],"mappings":";AAgBA,IAAM,eAAoC,CAAC;AAE3C,SAAS,oBAAoB,GAAkB;AAC7C,MAAI,EAAE,QAAQ,SAAU;AACxB,MAAI,aAAa,WAAW,EAAG;AAG/B,QAAM,MAAM,aAAa,aAAa,SAAS,CAAC;AAChD,MAAI,IAAI,gBAAiB;AAEzB,IAAE,gBAAgB;AAClB,MAAI,KAAK,UAAU,YAAY;AACjC;AAGA,IAAI,yBAAyB;AAE7B,SAAS,6BAA6B;AACpC,MAAI,uBAAwB;AAC5B,2BAAyB;AACzB,WAAS,iBAAiB,WAAW,mBAAmB;AAC1D;AAaO,IAAM,oBAAN,MAAwB;AAAA,EACrB;AAAA;AAAA,EACS;AAAA,EAKT,iBAAqD;AAAA,EACrD,SAAS;AAAA,EACT,UAAyB;AAAA;AAAA,EAChB,kBAAkB;AAAA,EAEnC,YAAY,MAAmB,MAAsB;AACnD,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,MACV,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK,aAAa;AAAA,MAC7B,cAAc,KAAK,gBAAgB;AAAA,IACrC;AAAA,EACF;AAAA,EAEA,WAAW;AACT,QAAI,KAAK,OAAQ;AACjB,SAAK,SAAS;AAEd,QAAI,KAAK,KAAK,WAAW;AACvB,iCAA2B;AAC3B,mBAAa,KAAK,IAAI;AAAA,IACxB;AAEA,QAAI,KAAK,KAAK,cAAc;AAC1B,WAAK,iBAAiB,CAAC,MAAoB;AAIzC,YACE,aAAa,SAAS,KACtB,aAAa,aAAa,SAAS,CAAC,MAAM,MAC1C;AACA;AAAA,QACF;AAEA,cAAM,SAAS,EAAE;AACjB,YAAI,KAAK,KAAK,SAAS,MAAM,EAAG;AAGhC,cAAM,aAAa,KAAK,KAAK;AAC7B,YAAI,YAAY;AACd,gBAAM,UAAU,SAAS;AAAA,YACvB,mBAAmB,UAAU;AAAA,UAC/B;AACA,cAAI,SAAS,SAAS,MAAM,EAAG;AAAA,QACjC;AACA,cAAM,YAAY,KAAK,KAAK,aAAa,iBAAiB;AAC1D,YAAI,WAAW;AACb,gBAAM,UAAU,SAAS,eAAe,SAAS;AACjD,cAAI,SAAS,SAAS,MAAM,EAAG;AAAA,QACjC;AAEA,YAAI,OAAO,KAAK,KAAK,iBAAiB,YAAY;AAChD,cAAI,CAAC,KAAK,KAAK,aAAa,CAAC,EAAG;AAAA,QAClC;AAEA,aAAK,KAAK,UAAU,eAAe;AAAA,MACrC;AAGA,WAAK,UAAU,sBAAsB,MAAM;AACzC,aAAK,UAAU;AACf,YAAI,KAAK,gBAAgB;AACvB,mBAAS,iBAAiB,eAAe,KAAK,cAAc;AAAA,QAC9D;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,aAAa;AACX,QAAI,CAAC,KAAK,OAAQ;AAClB,SAAK,SAAS;AAGd,UAAM,MAAM,aAAa,QAAQ,IAAI;AACrC,QAAI,QAAQ,GAAI,cAAa,OAAO,KAAK,CAAC;AAE1C,QAAI,KAAK,YAAY,MAAM;AACzB,2BAAqB,KAAK,OAAO;AACjC,WAAK,UAAU;AAAA,IACjB;AAEA,QAAI,KAAK,gBAAgB;AACvB,eAAS,oBAAoB,eAAe,KAAK,cAAc;AAC/D,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,WAAW;AACT,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
@@ -0,0 +1,24 @@
1
+ // src/portal.ts
2
+ function createPortal(content, id) {
3
+ const container = document.createElement("div");
4
+ container.setAttribute("data-base-astro-portal", id);
5
+ document.body.appendChild(container);
6
+ container.appendChild(content);
7
+ const cleanupHandler = () => {
8
+ if (container.parentElement) {
9
+ container.remove();
10
+ }
11
+ };
12
+ document.addEventListener("astro:before-preparation", cleanupHandler);
13
+ return () => {
14
+ document.removeEventListener("astro:before-preparation", cleanupHandler);
15
+ if (container.parentElement) {
16
+ container.remove();
17
+ }
18
+ };
19
+ }
20
+
21
+ export {
22
+ createPortal
23
+ };
24
+ //# sourceMappingURL=chunk-HN6LBLTZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portal.ts"],"sourcesContent":["/**\n * Moves an element to the end of `document.body` inside a portal container.\n *\n * Returns a cleanup function that removes the portal container and\n * unregisters the View Transitions cleanup listener.\n */\nexport function createPortal(content: HTMLElement, id: string): () => void {\n const container = document.createElement(\"div\");\n container.setAttribute(\"data-base-astro-portal\", id);\n document.body.appendChild(container);\n container.appendChild(content);\n\n // Clean up portals during Astro View Transitions\n const cleanupHandler = () => {\n if (container.parentElement) {\n container.remove();\n }\n };\n document.addEventListener(\"astro:before-preparation\", cleanupHandler);\n\n return () => {\n document.removeEventListener(\"astro:before-preparation\", cleanupHandler);\n if (container.parentElement) {\n container.remove();\n }\n };\n}\n"],"mappings":";AAMO,SAAS,aAAa,SAAsB,IAAwB;AACzE,QAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,YAAU,aAAa,0BAA0B,EAAE;AACnD,WAAS,KAAK,YAAY,SAAS;AACnC,YAAU,YAAY,OAAO;AAG7B,QAAM,iBAAiB,MAAM;AAC3B,QAAI,UAAU,eAAe;AAC3B,gBAAU,OAAO;AAAA,IACnB;AAAA,EACF;AACA,WAAS,iBAAiB,4BAA4B,cAAc;AAEpE,SAAO,MAAM;AACX,aAAS,oBAAoB,4BAA4B,cAAc;AACvE,QAAI,UAAU,eAAe;AAC3B,gBAAU,OAAO;AAAA,IACnB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,34 @@
1
+ // src/merge-props.ts
2
+ function mergeProps(...propSets) {
3
+ const result = {};
4
+ for (const props of propSets) {
5
+ if (!props) continue;
6
+ for (const [key, value] of Object.entries(props)) {
7
+ if (value === void 0) continue;
8
+ const isHandler = key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
9
+ if (isHandler && typeof value === "function") {
10
+ const existing = result[key];
11
+ if (existing) {
12
+ result[key] = (...args) => {
13
+ value(...args);
14
+ existing(...args);
15
+ };
16
+ } else {
17
+ result[key] = value;
18
+ }
19
+ } else if (key === "class" || key === "className") {
20
+ result[key] = [result[key], value].filter(Boolean).join(" ");
21
+ } else if (key === "style" && typeof value === "object" && value !== null) {
22
+ result[key] = { ...result[key], ...value };
23
+ } else {
24
+ result[key] = value;
25
+ }
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+
31
+ export {
32
+ mergeProps
33
+ };
34
+ //# sourceMappingURL=chunk-I6QJ4ATJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/merge-props.ts"],"sourcesContent":["type AnyProps = Record<string, unknown>;\n\n/**\n * Merges multiple prop objects, composing event handlers and\n * concatenating class names.\n *\n * - Event handlers (`on*`): all handlers run; latest set first.\n * - `class` / `className`: concatenated with spaces.\n * - `style` (object): shallow-merged, later values win.\n * - All other props: later values win.\n */\nexport function mergeProps<T extends AnyProps>(\n ...propSets: Array<T | undefined>\n): T {\n const result: AnyProps = {};\n\n for (const props of propSets) {\n if (!props) continue;\n\n for (const [key, value] of Object.entries(props)) {\n if (value === undefined) continue;\n\n const isHandler =\n key.length > 2 &&\n key.startsWith(\"on\") &&\n key[2] === key[2]?.toUpperCase();\n\n if (isHandler && typeof value === \"function\") {\n const existing = result[key] as\n | ((...args: unknown[]) => void)\n | undefined;\n if (existing) {\n result[key] = (...args: unknown[]) => {\n (value as (...a: unknown[]) => void)(...args);\n existing(...args);\n };\n } else {\n result[key] = value;\n }\n } else if (key === \"class\" || key === \"className\") {\n result[key] = [result[key], value].filter(Boolean).join(\" \");\n } else if (\n key === \"style\" &&\n typeof value === \"object\" &&\n value !== null\n ) {\n result[key] = { ...(result[key] as object | undefined), ...value };\n } else {\n result[key] = value;\n }\n }\n }\n\n return result as T;\n}\n"],"mappings":";AAWO,SAAS,cACX,UACA;AACH,QAAM,SAAmB,CAAC;AAE1B,aAAW,SAAS,UAAU;AAC5B,QAAI,CAAC,MAAO;AAEZ,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,UAAI,UAAU,OAAW;AAEzB,YAAM,YACJ,IAAI,SAAS,KACb,IAAI,WAAW,IAAI,KACnB,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,YAAY;AAEjC,UAAI,aAAa,OAAO,UAAU,YAAY;AAC5C,cAAM,WAAW,OAAO,GAAG;AAG3B,YAAI,UAAU;AACZ,iBAAO,GAAG,IAAI,IAAI,SAAoB;AACpC,YAAC,MAAoC,GAAG,IAAI;AAC5C,qBAAS,GAAG,IAAI;AAAA,UAClB;AAAA,QACF,OAAO;AACL,iBAAO,GAAG,IAAI;AAAA,QAChB;AAAA,MACF,WAAW,QAAQ,WAAW,QAAQ,aAAa;AACjD,eAAO,GAAG,IAAI,CAAC,OAAO,GAAG,GAAG,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,MAC7D,WACE,QAAQ,WACR,OAAO,UAAU,YACjB,UAAU,MACV;AACA,eAAO,GAAG,IAAI,EAAE,GAAI,OAAO,GAAG,GAA0B,GAAG,MAAM;AAAA,MACnE,OAAO;AACL,eAAO,GAAG,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,113 @@
1
+ // src/composite.ts
2
+ function isDisabled(el) {
3
+ return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
4
+ }
5
+ function findNonDisabledIndex(items, startIndex, decrement) {
6
+ let index = startIndex;
7
+ do {
8
+ index += decrement ? -1 : 1;
9
+ } while (index >= 0 && index < items.length && isDisabled(items[index]));
10
+ return index;
11
+ }
12
+ var CompositeNavigation = class {
13
+ container;
14
+ itemSelector;
15
+ opts;
16
+ keydownHandler = null;
17
+ active = false;
18
+ constructor(container, itemSelector, opts = {}) {
19
+ this.container = container;
20
+ this.itemSelector = itemSelector;
21
+ this.opts = {
22
+ orientation: opts.orientation ?? "horizontal",
23
+ loop: opts.loop ?? true,
24
+ homeEnd: opts.homeEnd ?? true,
25
+ onHighlightedIndexChange: opts.onHighlightedIndexChange ?? (() => {
26
+ })
27
+ };
28
+ }
29
+ getItems() {
30
+ return Array.from(
31
+ this.container.querySelectorAll(this.itemSelector)
32
+ );
33
+ }
34
+ isForwardKey(key, isRtl) {
35
+ const { orientation } = this.opts;
36
+ if (orientation === "horizontal" || orientation === "both") {
37
+ if (key === (isRtl ? "ArrowLeft" : "ArrowRight")) return true;
38
+ }
39
+ if (orientation === "vertical" || orientation === "both") {
40
+ if (key === "ArrowDown") return true;
41
+ }
42
+ return false;
43
+ }
44
+ isBackwardKey(key, isRtl) {
45
+ const { orientation } = this.opts;
46
+ if (orientation === "horizontal" || orientation === "both") {
47
+ if (key === (isRtl ? "ArrowRight" : "ArrowLeft")) return true;
48
+ }
49
+ if (orientation === "vertical" || orientation === "both") {
50
+ if (key === "ArrowUp") return true;
51
+ }
52
+ return false;
53
+ }
54
+ activate() {
55
+ if (this.active) return;
56
+ this.active = true;
57
+ this.keydownHandler = (e) => {
58
+ const items = this.getItems();
59
+ if (items.length === 0) return;
60
+ if (e.ctrlKey || e.altKey || e.metaKey) return;
61
+ const isRtl = getComputedStyle(this.container).direction === "rtl";
62
+ const currentIndex = items.indexOf(document.activeElement);
63
+ if (currentIndex === -1) return;
64
+ let nextIndex = null;
65
+ if (this.isForwardKey(e.key, isRtl)) {
66
+ nextIndex = findNonDisabledIndex(items, currentIndex, false);
67
+ if (nextIndex >= items.length) {
68
+ nextIndex = this.opts.loop ? findNonDisabledIndex(items, -1, false) : currentIndex;
69
+ }
70
+ } else if (this.isBackwardKey(e.key, isRtl)) {
71
+ nextIndex = findNonDisabledIndex(items, currentIndex, true);
72
+ if (nextIndex < 0) {
73
+ nextIndex = this.opts.loop ? findNonDisabledIndex(items, items.length, true) : currentIndex;
74
+ }
75
+ } else if (this.opts.homeEnd && e.key === "Home") {
76
+ nextIndex = findNonDisabledIndex(items, -1, false);
77
+ } else if (this.opts.homeEnd && e.key === "End") {
78
+ nextIndex = findNonDisabledIndex(items, items.length, true);
79
+ }
80
+ if (nextIndex === null) return;
81
+ if (nextIndex < 0 || nextIndex >= items.length) return;
82
+ if (nextIndex === currentIndex) return;
83
+ e.preventDefault();
84
+ items[currentIndex].setAttribute("tabindex", "-1");
85
+ items[nextIndex].setAttribute("tabindex", "0");
86
+ items[nextIndex].focus();
87
+ this.opts.onHighlightedIndexChange(nextIndex);
88
+ };
89
+ this.container.addEventListener("keydown", this.keydownHandler);
90
+ }
91
+ deactivate() {
92
+ if (!this.active) return;
93
+ this.active = false;
94
+ if (this.keydownHandler) {
95
+ this.container.removeEventListener("keydown", this.keydownHandler);
96
+ this.keydownHandler = null;
97
+ }
98
+ }
99
+ /** Set the roving tabindex so only the given index is tabbable. */
100
+ setActiveIndex(items, index) {
101
+ items.forEach((item, i) => {
102
+ item.setAttribute("tabindex", i === index ? "0" : "-1");
103
+ });
104
+ }
105
+ isActive() {
106
+ return this.active;
107
+ }
108
+ };
109
+
110
+ export {
111
+ CompositeNavigation
112
+ };
113
+ //# sourceMappingURL=chunk-LN2MVY72.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/composite.ts"],"sourcesContent":["export type Orientation = \"horizontal\" | \"vertical\" | \"both\";\n\nexport interface CompositeOptions {\n /** Which arrow keys navigate. Defaults to \"horizontal\". */\n orientation?: Orientation;\n /** Whether navigation wraps from end to start. Defaults to true. */\n loop?: boolean;\n /** Whether Home/End keys are handled. Defaults to true. */\n homeEnd?: boolean;\n /** Called when the highlighted index changes. */\n onHighlightedIndexChange?: (index: number) => void;\n}\n\n/**\n * Returns true if the element should be considered disabled.\n * Checks `disabled` attribute and `aria-disabled=\"true\"`.\n */\nfunction isDisabled(el: HTMLElement): boolean {\n return (\n el.hasAttribute(\"disabled\") || el.getAttribute(\"aria-disabled\") === \"true\"\n );\n}\n\n/**\n * Finds the next non-disabled index in a list, walking forward or backward.\n */\nfunction findNonDisabledIndex(\n items: HTMLElement[],\n startIndex: number,\n decrement: boolean,\n): number {\n let index = startIndex;\n do {\n index += decrement ? -1 : 1;\n } while (index >= 0 && index < items.length && isDisabled(items[index]));\n return index;\n}\n\n/**\n * Manages roving tabindex keyboard navigation for a list of items.\n *\n * Ported from Base UI's composite navigation pattern. Handles arrow keys,\n * Home/End, disabled item skipping, orientation, RTL, and loop wrapping.\n *\n * Used by: Tabs, Menu, Select, RadioGroup, ToggleGroup.\n */\nexport class CompositeNavigation {\n private container: HTMLElement;\n private itemSelector: string;\n private opts: Required<CompositeOptions>;\n private keydownHandler: ((e: KeyboardEvent) => void) | null = null;\n private active = false;\n\n constructor(\n container: HTMLElement,\n itemSelector: string,\n opts: CompositeOptions = {},\n ) {\n this.container = container;\n this.itemSelector = itemSelector;\n this.opts = {\n orientation: opts.orientation ?? \"horizontal\",\n loop: opts.loop ?? true,\n homeEnd: opts.homeEnd ?? true,\n onHighlightedIndexChange: opts.onHighlightedIndexChange ?? (() => {}),\n };\n }\n\n private getItems(): HTMLElement[] {\n return Array.from(\n this.container.querySelectorAll(this.itemSelector),\n ) as HTMLElement[];\n }\n\n private isForwardKey(key: string, isRtl: boolean): boolean {\n const { orientation } = this.opts;\n if (orientation === \"horizontal\" || orientation === \"both\") {\n if (key === (isRtl ? \"ArrowLeft\" : \"ArrowRight\")) return true;\n }\n if (orientation === \"vertical\" || orientation === \"both\") {\n if (key === \"ArrowDown\") return true;\n }\n return false;\n }\n\n private isBackwardKey(key: string, isRtl: boolean): boolean {\n const { orientation } = this.opts;\n if (orientation === \"horizontal\" || orientation === \"both\") {\n if (key === (isRtl ? \"ArrowRight\" : \"ArrowLeft\")) return true;\n }\n if (orientation === \"vertical\" || orientation === \"both\") {\n if (key === \"ArrowUp\") return true;\n }\n return false;\n }\n\n activate() {\n if (this.active) return;\n this.active = true;\n\n this.keydownHandler = (e: KeyboardEvent) => {\n const items = this.getItems();\n if (items.length === 0) return;\n\n // Skip if modifier keys are held\n if (e.ctrlKey || e.altKey || e.metaKey) return;\n\n const isRtl = getComputedStyle(this.container).direction === \"rtl\";\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n if (currentIndex === -1) return;\n\n let nextIndex: number | null = null;\n\n if (this.isForwardKey(e.key, isRtl)) {\n nextIndex = findNonDisabledIndex(items, currentIndex, false);\n if (nextIndex >= items.length) {\n nextIndex = this.opts.loop\n ? findNonDisabledIndex(items, -1, false)\n : currentIndex;\n }\n } else if (this.isBackwardKey(e.key, isRtl)) {\n nextIndex = findNonDisabledIndex(items, currentIndex, true);\n if (nextIndex < 0) {\n nextIndex = this.opts.loop\n ? findNonDisabledIndex(items, items.length, true)\n : currentIndex;\n }\n } else if (this.opts.homeEnd && e.key === \"Home\") {\n nextIndex = findNonDisabledIndex(items, -1, false);\n } else if (this.opts.homeEnd && e.key === \"End\") {\n nextIndex = findNonDisabledIndex(items, items.length, true);\n }\n\n if (nextIndex === null) return;\n if (nextIndex < 0 || nextIndex >= items.length) return;\n if (nextIndex === currentIndex) return;\n\n e.preventDefault();\n\n // Update roving tabindex\n items[currentIndex].setAttribute(\"tabindex\", \"-1\");\n items[nextIndex].setAttribute(\"tabindex\", \"0\");\n items[nextIndex].focus();\n\n this.opts.onHighlightedIndexChange(nextIndex);\n };\n\n this.container.addEventListener(\"keydown\", this.keydownHandler);\n }\n\n deactivate() {\n if (!this.active) return;\n this.active = false;\n\n if (this.keydownHandler) {\n this.container.removeEventListener(\"keydown\", this.keydownHandler);\n this.keydownHandler = null;\n }\n }\n\n /** Set the roving tabindex so only the given index is tabbable. */\n setActiveIndex(items: HTMLElement[], index: number) {\n items.forEach((item, i) => {\n item.setAttribute(\"tabindex\", i === index ? \"0\" : \"-1\");\n });\n }\n\n isActive() {\n return this.active;\n }\n}\n"],"mappings":";AAiBA,SAAS,WAAW,IAA0B;AAC5C,SACE,GAAG,aAAa,UAAU,KAAK,GAAG,aAAa,eAAe,MAAM;AAExE;AAKA,SAAS,qBACP,OACA,YACA,WACQ;AACR,MAAI,QAAQ;AACZ,KAAG;AACD,aAAS,YAAY,KAAK;AAAA,EAC5B,SAAS,SAAS,KAAK,QAAQ,MAAM,UAAU,WAAW,MAAM,KAAK,CAAC;AACtE,SAAO;AACT;AAUO,IAAM,sBAAN,MAA0B;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAsD;AAAA,EACtD,SAAS;AAAA,EAEjB,YACE,WACA,cACA,OAAyB,CAAC,GAC1B;AACA,SAAK,YAAY;AACjB,SAAK,eAAe;AACpB,SAAK,OAAO;AAAA,MACV,aAAa,KAAK,eAAe;AAAA,MACjC,MAAM,KAAK,QAAQ;AAAA,MACnB,SAAS,KAAK,WAAW;AAAA,MACzB,0BAA0B,KAAK,6BAA6B,MAAM;AAAA,MAAC;AAAA,IACrE;AAAA,EACF;AAAA,EAEQ,WAA0B;AAChC,WAAO,MAAM;AAAA,MACX,KAAK,UAAU,iBAAiB,KAAK,YAAY;AAAA,IACnD;AAAA,EACF;AAAA,EAEQ,aAAa,KAAa,OAAyB;AACzD,UAAM,EAAE,YAAY,IAAI,KAAK;AAC7B,QAAI,gBAAgB,gBAAgB,gBAAgB,QAAQ;AAC1D,UAAI,SAAS,QAAQ,cAAc,cAAe,QAAO;AAAA,IAC3D;AACA,QAAI,gBAAgB,cAAc,gBAAgB,QAAQ;AACxD,UAAI,QAAQ,YAAa,QAAO;AAAA,IAClC;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,cAAc,KAAa,OAAyB;AAC1D,UAAM,EAAE,YAAY,IAAI,KAAK;AAC7B,QAAI,gBAAgB,gBAAgB,gBAAgB,QAAQ;AAC1D,UAAI,SAAS,QAAQ,eAAe,aAAc,QAAO;AAAA,IAC3D;AACA,QAAI,gBAAgB,cAAc,gBAAgB,QAAQ;AACxD,UAAI,QAAQ,UAAW,QAAO;AAAA,IAChC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,WAAW;AACT,QAAI,KAAK,OAAQ;AACjB,SAAK,SAAS;AAEd,SAAK,iBAAiB,CAAC,MAAqB;AAC1C,YAAM,QAAQ,KAAK,SAAS;AAC5B,UAAI,MAAM,WAAW,EAAG;AAGxB,UAAI,EAAE,WAAW,EAAE,UAAU,EAAE,QAAS;AAExC,YAAM,QAAQ,iBAAiB,KAAK,SAAS,EAAE,cAAc;AAC7D,YAAM,eAAe,MAAM,QAAQ,SAAS,aAA4B;AACxE,UAAI,iBAAiB,GAAI;AAEzB,UAAI,YAA2B;AAE/B,UAAI,KAAK,aAAa,EAAE,KAAK,KAAK,GAAG;AACnC,oBAAY,qBAAqB,OAAO,cAAc,KAAK;AAC3D,YAAI,aAAa,MAAM,QAAQ;AAC7B,sBAAY,KAAK,KAAK,OAClB,qBAAqB,OAAO,IAAI,KAAK,IACrC;AAAA,QACN;AAAA,MACF,WAAW,KAAK,cAAc,EAAE,KAAK,KAAK,GAAG;AAC3C,oBAAY,qBAAqB,OAAO,cAAc,IAAI;AAC1D,YAAI,YAAY,GAAG;AACjB,sBAAY,KAAK,KAAK,OAClB,qBAAqB,OAAO,MAAM,QAAQ,IAAI,IAC9C;AAAA,QACN;AAAA,MACF,WAAW,KAAK,KAAK,WAAW,EAAE,QAAQ,QAAQ;AAChD,oBAAY,qBAAqB,OAAO,IAAI,KAAK;AAAA,MACnD,WAAW,KAAK,KAAK,WAAW,EAAE,QAAQ,OAAO;AAC/C,oBAAY,qBAAqB,OAAO,MAAM,QAAQ,IAAI;AAAA,MAC5D;AAEA,UAAI,cAAc,KAAM;AACxB,UAAI,YAAY,KAAK,aAAa,MAAM,OAAQ;AAChD,UAAI,cAAc,aAAc;AAEhC,QAAE,eAAe;AAGjB,YAAM,YAAY,EAAE,aAAa,YAAY,IAAI;AACjD,YAAM,SAAS,EAAE,aAAa,YAAY,GAAG;AAC7C,YAAM,SAAS,EAAE,MAAM;AAEvB,WAAK,KAAK,yBAAyB,SAAS;AAAA,IAC9C;AAEA,SAAK,UAAU,iBAAiB,WAAW,KAAK,cAAc;AAAA,EAChE;AAAA,EAEA,aAAa;AACX,QAAI,CAAC,KAAK,OAAQ;AAClB,SAAK,SAAS;AAEd,QAAI,KAAK,gBAAgB;AACvB,WAAK,UAAU,oBAAoB,WAAW,KAAK,cAAc;AACjE,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGA,eAAe,OAAsB,OAAe;AAClD,UAAM,QAAQ,CAAC,MAAM,MAAM;AACzB,WAAK,aAAa,YAAY,MAAM,QAAQ,MAAM,IAAI;AAAA,IACxD,CAAC;AAAA,EACH;AAAA,EAEA,WAAW;AACT,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
@@ -0,0 +1,79 @@
1
+ // src/focus-trap.ts
2
+ import { tabbable } from "tabbable";
3
+ var FocusTrap = class {
4
+ root;
5
+ opts;
6
+ previouslyFocused = null;
7
+ keydownHandler = null;
8
+ active = false;
9
+ constructor(root, opts = {}) {
10
+ this.root = root;
11
+ this.opts = {
12
+ initialFocus: opts.initialFocus ?? "first",
13
+ returnFocus: opts.returnFocus ?? true
14
+ };
15
+ }
16
+ activate() {
17
+ if (this.active) return;
18
+ this.active = true;
19
+ this.previouslyFocused = document.activeElement;
20
+ this.keydownHandler = (e) => {
21
+ if (e.key !== "Tab") return;
22
+ const targets = tabbable(this.root);
23
+ if (targets.length === 0) {
24
+ e.preventDefault();
25
+ return;
26
+ }
27
+ const first = targets[0];
28
+ const last = targets[targets.length - 1];
29
+ if (e.shiftKey) {
30
+ if (document.activeElement === first) {
31
+ last.focus();
32
+ e.preventDefault();
33
+ }
34
+ } else {
35
+ if (document.activeElement === last) {
36
+ first.focus();
37
+ e.preventDefault();
38
+ }
39
+ }
40
+ };
41
+ document.addEventListener("keydown", this.keydownHandler);
42
+ requestAnimationFrame(() => {
43
+ if (!this.active) return;
44
+ const targets = tabbable(this.root);
45
+ if (this.opts.initialFocus instanceof HTMLElement && this.root.contains(this.opts.initialFocus)) {
46
+ this.opts.initialFocus.focus();
47
+ } else if (this.opts.initialFocus === "container") {
48
+ if (!this.root.hasAttribute("tabindex")) {
49
+ this.root.setAttribute("tabindex", "-1");
50
+ }
51
+ this.root.focus();
52
+ } else {
53
+ targets[0]?.focus();
54
+ }
55
+ });
56
+ }
57
+ deactivate() {
58
+ if (!this.active) return;
59
+ this.active = false;
60
+ if (this.keydownHandler) {
61
+ document.removeEventListener("keydown", this.keydownHandler);
62
+ this.keydownHandler = null;
63
+ }
64
+ if (this.opts.returnFocus && this.previouslyFocused) {
65
+ if (document.contains(this.previouslyFocused)) {
66
+ this.previouslyFocused.focus();
67
+ }
68
+ }
69
+ this.previouslyFocused = null;
70
+ }
71
+ isActive() {
72
+ return this.active;
73
+ }
74
+ };
75
+
76
+ export {
77
+ FocusTrap
78
+ };
79
+ //# sourceMappingURL=chunk-M46BYUQY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/focus-trap.ts"],"sourcesContent":["import { tabbable } from \"tabbable\";\n\nexport interface FocusTrapOptions {\n /** Where to place focus when the trap activates. Defaults to \"first\". */\n initialFocus?: HTMLElement | \"first\" | \"container\";\n /** Whether to return focus to the previously focused element on deactivate. Defaults to true. */\n returnFocus?: boolean;\n}\n\n/**\n * Traps keyboard focus within a container element.\n *\n * Re-queries tabbable elements on every Tab press so dynamically\n * added/removed elements are handled correctly. Uses the `tabbable`\n * library for accurate focusable-element detection.\n */\nexport class FocusTrap {\n private root: HTMLElement;\n private opts: Required<FocusTrapOptions>;\n private previouslyFocused: HTMLElement | null = null;\n private keydownHandler: ((e: KeyboardEvent) => void) | null = null;\n private active = false;\n\n constructor(root: HTMLElement, opts: FocusTrapOptions = {}) {\n this.root = root;\n this.opts = {\n initialFocus: opts.initialFocus ?? \"first\",\n returnFocus: opts.returnFocus ?? true,\n };\n }\n\n activate() {\n if (this.active) return;\n this.active = true;\n this.previouslyFocused = document.activeElement as HTMLElement;\n\n this.keydownHandler = (e: KeyboardEvent) => {\n if (e.key !== \"Tab\") return;\n\n // Re-query on every Tab to handle dynamic content\n const targets = tabbable(this.root);\n if (targets.length === 0) {\n // Nothing tabbable — prevent Tab from leaving\n e.preventDefault();\n return;\n }\n\n const first = targets[0] as HTMLElement;\n const last = targets[targets.length - 1] as HTMLElement;\n\n if (e.shiftKey) {\n if (document.activeElement === first) {\n last.focus();\n e.preventDefault();\n }\n } else {\n if (document.activeElement === last) {\n first.focus();\n e.preventDefault();\n }\n }\n };\n\n document.addEventListener(\"keydown\", this.keydownHandler);\n\n // Set initial focus\n requestAnimationFrame(() => {\n if (!this.active) return;\n const targets = tabbable(this.root);\n\n if (\n this.opts.initialFocus instanceof HTMLElement &&\n this.root.contains(this.opts.initialFocus)\n ) {\n this.opts.initialFocus.focus();\n } else if (this.opts.initialFocus === \"container\") {\n if (!this.root.hasAttribute(\"tabindex\")) {\n this.root.setAttribute(\"tabindex\", \"-1\");\n }\n this.root.focus();\n } else {\n // \"first\" — default\n (targets[0] as HTMLElement | undefined)?.focus();\n }\n });\n }\n\n deactivate() {\n if (!this.active) return;\n this.active = false;\n\n if (this.keydownHandler) {\n document.removeEventListener(\"keydown\", this.keydownHandler);\n this.keydownHandler = null;\n }\n\n if (this.opts.returnFocus && this.previouslyFocused) {\n if (document.contains(this.previouslyFocused)) {\n this.previouslyFocused.focus();\n }\n }\n this.previouslyFocused = null;\n }\n\n isActive() {\n return this.active;\n }\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAgBlB,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACA;AAAA,EACA,oBAAwC;AAAA,EACxC,iBAAsD;AAAA,EACtD,SAAS;AAAA,EAEjB,YAAY,MAAmB,OAAyB,CAAC,GAAG;AAC1D,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,MACV,cAAc,KAAK,gBAAgB;AAAA,MACnC,aAAa,KAAK,eAAe;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,WAAW;AACT,QAAI,KAAK,OAAQ;AACjB,SAAK,SAAS;AACd,SAAK,oBAAoB,SAAS;AAElC,SAAK,iBAAiB,CAAC,MAAqB;AAC1C,UAAI,EAAE,QAAQ,MAAO;AAGrB,YAAM,UAAU,SAAS,KAAK,IAAI;AAClC,UAAI,QAAQ,WAAW,GAAG;AAExB,UAAE,eAAe;AACjB;AAAA,MACF;AAEA,YAAM,QAAQ,QAAQ,CAAC;AACvB,YAAM,OAAO,QAAQ,QAAQ,SAAS,CAAC;AAEvC,UAAI,EAAE,UAAU;AACd,YAAI,SAAS,kBAAkB,OAAO;AACpC,eAAK,MAAM;AACX,YAAE,eAAe;AAAA,QACnB;AAAA,MACF,OAAO;AACL,YAAI,SAAS,kBAAkB,MAAM;AACnC,gBAAM,MAAM;AACZ,YAAE,eAAe;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAEA,aAAS,iBAAiB,WAAW,KAAK,cAAc;AAGxD,0BAAsB,MAAM;AAC1B,UAAI,CAAC,KAAK,OAAQ;AAClB,YAAM,UAAU,SAAS,KAAK,IAAI;AAElC,UACE,KAAK,KAAK,wBAAwB,eAClC,KAAK,KAAK,SAAS,KAAK,KAAK,YAAY,GACzC;AACA,aAAK,KAAK,aAAa,MAAM;AAAA,MAC/B,WAAW,KAAK,KAAK,iBAAiB,aAAa;AACjD,YAAI,CAAC,KAAK,KAAK,aAAa,UAAU,GAAG;AACvC,eAAK,KAAK,aAAa,YAAY,IAAI;AAAA,QACzC;AACA,aAAK,KAAK,MAAM;AAAA,MAClB,OAAO;AAEL,QAAC,QAAQ,CAAC,GAA+B,MAAM;AAAA,MACjD;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,aAAa;AACX,QAAI,CAAC,KAAK,OAAQ;AAClB,SAAK,SAAS;AAEd,QAAI,KAAK,gBAAgB;AACvB,eAAS,oBAAoB,WAAW,KAAK,cAAc;AAC3D,WAAK,iBAAiB;AAAA,IACxB;AAEA,QAAI,KAAK,KAAK,eAAe,KAAK,mBAAmB;AACnD,UAAI,SAAS,SAAS,KAAK,iBAAiB,GAAG;AAC7C,aAAK,kBAAkB,MAAM;AAAA,MAC/B;AAAA,IACF;AACA,SAAK,oBAAoB;AAAA,EAC3B;AAAA,EAEA,WAAW;AACT,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
@@ -0,0 +1,35 @@
1
+ // src/inert.ts
2
+ function markOthersInert(element) {
3
+ const restore = [];
4
+ let current = element;
5
+ while (current && current !== document.body) {
6
+ const parent = current.parentElement;
7
+ if (!parent) break;
8
+ for (let i = 0; i < parent.children.length; i++) {
9
+ const sibling = parent.children[i];
10
+ if (sibling === current) continue;
11
+ if (sibling.contains(element)) continue;
12
+ const tag = sibling.tagName;
13
+ if (tag === "SCRIPT" || tag === "STYLE" || tag === "LINK") continue;
14
+ const prev = sibling.getAttribute("aria-hidden");
15
+ if (prev === "true") continue;
16
+ restore.push({ el: sibling, prev });
17
+ sibling.setAttribute("aria-hidden", "true");
18
+ }
19
+ current = parent;
20
+ }
21
+ return () => {
22
+ for (const { el, prev } of restore) {
23
+ if (prev === null) {
24
+ el.removeAttribute("aria-hidden");
25
+ } else {
26
+ el.setAttribute("aria-hidden", prev);
27
+ }
28
+ }
29
+ };
30
+ }
31
+
32
+ export {
33
+ markOthersInert
34
+ };
35
+ //# sourceMappingURL=chunk-N2FDXX6C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/inert.ts"],"sourcesContent":["/**\n * Marks all elements outside the given element as `aria-hidden=\"true\"`,\n * making only the target element visible to screen readers.\n *\n * Returns a cleanup function that restores the original `aria-hidden`\n * values. Handles elements that already had `aria-hidden` set.\n */\nexport function markOthersInert(element: HTMLElement): () => void {\n const restore: Array<{ el: Element; prev: string | null }> = [];\n\n // Walk up to body, hiding siblings at each level\n let current: Element | null = element;\n while (current && current !== document.body) {\n const parent: Element | null = current.parentElement;\n if (!parent) break;\n\n for (let i = 0; i < parent.children.length; i++) {\n const sibling = parent.children[i];\n if (sibling === current) continue;\n if (sibling.contains(element)) continue;\n\n // Skip script/style elements\n const tag = sibling.tagName;\n if (tag === \"SCRIPT\" || tag === \"STYLE\" || tag === \"LINK\") continue;\n\n const prev = sibling.getAttribute(\"aria-hidden\");\n if (prev === \"true\") continue; // already hidden, don't touch\n\n restore.push({ el: sibling, prev });\n sibling.setAttribute(\"aria-hidden\", \"true\");\n }\n\n current = parent;\n }\n\n return () => {\n for (const { el, prev } of restore) {\n if (prev === null) {\n el.removeAttribute(\"aria-hidden\");\n } else {\n el.setAttribute(\"aria-hidden\", prev);\n }\n }\n };\n}\n"],"mappings":";AAOO,SAAS,gBAAgB,SAAkC;AAChE,QAAM,UAAuD,CAAC;AAG9D,MAAI,UAA0B;AAC9B,SAAO,WAAW,YAAY,SAAS,MAAM;AAC3C,UAAM,SAAyB,QAAQ;AACvC,QAAI,CAAC,OAAQ;AAEb,aAAS,IAAI,GAAG,IAAI,OAAO,SAAS,QAAQ,KAAK;AAC/C,YAAM,UAAU,OAAO,SAAS,CAAC;AACjC,UAAI,YAAY,QAAS;AACzB,UAAI,QAAQ,SAAS,OAAO,EAAG;AAG/B,YAAM,MAAM,QAAQ;AACpB,UAAI,QAAQ,YAAY,QAAQ,WAAW,QAAQ,OAAQ;AAE3D,YAAM,OAAO,QAAQ,aAAa,aAAa;AAC/C,UAAI,SAAS,OAAQ;AAErB,cAAQ,KAAK,EAAE,IAAI,SAAS,KAAK,CAAC;AAClC,cAAQ,aAAa,eAAe,MAAM;AAAA,IAC5C;AAEA,cAAU;AAAA,EACZ;AAEA,SAAO,MAAM;AACX,eAAW,EAAE,IAAI,KAAK,KAAK,SAAS;AAClC,UAAI,SAAS,MAAM;AACjB,WAAG,gBAAgB,aAAa;AAAA,MAClC,OAAO;AACL,WAAG,aAAa,eAAe,IAAI;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,48 @@
1
+ // src/typeahead.ts
2
+ var Typeahead = class {
3
+ opts;
4
+ buffer = "";
5
+ timerId = null;
6
+ timeout;
7
+ constructor(opts) {
8
+ this.opts = opts;
9
+ this.timeout = opts.timeout ?? 1e3;
10
+ }
11
+ /**
12
+ * Handle a keydown event. Call this from your component's keydown handler.
13
+ * Returns true if the key was consumed (a printable character), false otherwise.
14
+ */
15
+ handle(key) {
16
+ if (key.length !== 1) return false;
17
+ if (this.timerId !== null) {
18
+ clearTimeout(this.timerId);
19
+ }
20
+ this.buffer += key.toLowerCase();
21
+ this.timerId = setTimeout(() => {
22
+ this.buffer = "";
23
+ this.timerId = null;
24
+ }, this.timeout);
25
+ const count = this.opts.getItemCount();
26
+ for (let i = 0; i < count; i++) {
27
+ const text = this.opts.getItemText(i).toLowerCase();
28
+ if (text.startsWith(this.buffer)) {
29
+ this.opts.onMatch(i);
30
+ return true;
31
+ }
32
+ }
33
+ return true;
34
+ }
35
+ /** Reset the buffer immediately. */
36
+ reset() {
37
+ this.buffer = "";
38
+ if (this.timerId !== null) {
39
+ clearTimeout(this.timerId);
40
+ this.timerId = null;
41
+ }
42
+ }
43
+ };
44
+
45
+ export {
46
+ Typeahead
47
+ };
48
+ //# sourceMappingURL=chunk-OKZYYVLM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/typeahead.ts"],"sourcesContent":["export interface TypeaheadOptions {\n /** Called when a match is found. Receives the matching index. */\n onMatch: (index: number) => void;\n /** Returns the text content for the item at the given index. */\n getItemText: (index: number) => string;\n /** Total number of items. */\n getItemCount: () => number;\n /** Timeout in ms before the search buffer resets. Defaults to 1000. */\n timeout?: number;\n}\n\n/**\n * Handles type-to-select behavior for lists.\n *\n * Accumulates typed characters into a buffer. On each keystroke, searches\n * for an item whose text starts with the buffer (case-insensitive).\n * Resets the buffer after a configurable timeout of inactivity.\n *\n * Used by: Select, Menu, Combobox, Listbox.\n */\nexport class Typeahead {\n private opts: TypeaheadOptions;\n private buffer = \"\";\n private timerId: ReturnType<typeof setTimeout> | null = null;\n private timeout: number;\n\n constructor(opts: TypeaheadOptions) {\n this.opts = opts;\n this.timeout = opts.timeout ?? 1000;\n }\n\n /**\n * Handle a keydown event. Call this from your component's keydown handler.\n * Returns true if the key was consumed (a printable character), false otherwise.\n */\n handle(key: string): boolean {\n // Only handle single printable characters\n if (key.length !== 1) return false;\n\n // Reset timer\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n }\n\n this.buffer += key.toLowerCase();\n\n this.timerId = setTimeout(() => {\n this.buffer = \"\";\n this.timerId = null;\n }, this.timeout);\n\n // Search for a match\n const count = this.opts.getItemCount();\n for (let i = 0; i < count; i++) {\n const text = this.opts.getItemText(i).toLowerCase();\n if (text.startsWith(this.buffer)) {\n this.opts.onMatch(i);\n return true;\n }\n }\n\n return true; // consumed the character even if no match\n }\n\n /** Reset the buffer immediately. */\n reset() {\n this.buffer = \"\";\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n this.timerId = null;\n }\n }\n}\n"],"mappings":";AAoBO,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACA,SAAS;AAAA,EACT,UAAgD;AAAA,EAChD;AAAA,EAER,YAAY,MAAwB;AAClC,SAAK,OAAO;AACZ,SAAK,UAAU,KAAK,WAAW;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,KAAsB;AAE3B,QAAI,IAAI,WAAW,EAAG,QAAO;AAG7B,QAAI,KAAK,YAAY,MAAM;AACzB,mBAAa,KAAK,OAAO;AAAA,IAC3B;AAEA,SAAK,UAAU,IAAI,YAAY;AAE/B,SAAK,UAAU,WAAW,MAAM;AAC9B,WAAK,SAAS;AACd,WAAK,UAAU;AAAA,IACjB,GAAG,KAAK,OAAO;AAGf,UAAM,QAAQ,KAAK,KAAK,aAAa;AACrC,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,OAAO,KAAK,KAAK,YAAY,CAAC,EAAE,YAAY;AAClD,UAAI,KAAK,WAAW,KAAK,MAAM,GAAG;AAChC,aAAK,KAAK,QAAQ,CAAC;AACnB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,QAAQ;AACN,SAAK,SAAS;AACd,QAAI,KAAK,YAAY,MAAM;AACzB,mBAAa,KAAK,OAAO;AACzB,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,54 @@
1
+ // src/floating.ts
2
+ import {
3
+ computePosition,
4
+ flip,
5
+ shift,
6
+ offset as offsetMiddleware,
7
+ size,
8
+ autoUpdate
9
+ } from "@floating-ui/dom";
10
+ function setupFloating(reference, floating, opts = {}) {
11
+ const {
12
+ placement = "bottom-start",
13
+ strategy = "absolute",
14
+ offset = 4,
15
+ flip: enableFlip = true,
16
+ shift: enableShift = true,
17
+ padding = 8,
18
+ matchWidth = false
19
+ } = opts;
20
+ const middleware = [
21
+ offsetMiddleware(offset),
22
+ ...enableFlip ? [flip({ padding })] : [],
23
+ ...enableShift ? [shift({ padding })] : [],
24
+ ...matchWidth ? [
25
+ size({
26
+ apply({ rects }) {
27
+ Object.assign(floating.style, {
28
+ width: `${rects.reference.width}px`
29
+ });
30
+ }
31
+ })
32
+ ] : []
33
+ ];
34
+ floating.style.position = strategy;
35
+ const cleanup = autoUpdate(reference, floating, async () => {
36
+ const result = await computePosition(reference, floating, {
37
+ placement,
38
+ strategy,
39
+ middleware
40
+ });
41
+ Object.assign(floating.style, {
42
+ left: `${result.x}px`,
43
+ top: `${result.y}px`
44
+ });
45
+ const side = result.placement.split("-")[0];
46
+ floating.setAttribute("data-side", side);
47
+ });
48
+ return cleanup;
49
+ }
50
+
51
+ export {
52
+ setupFloating
53
+ };
54
+ //# sourceMappingURL=chunk-Q2HX4RL6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/floating.ts"],"sourcesContent":["import {\n computePosition,\n flip,\n shift,\n offset as offsetMiddleware,\n size,\n autoUpdate,\n type Placement,\n type Strategy,\n} from \"@floating-ui/dom\";\n\nexport interface FloatingOptions {\n /** Placement relative to the reference element. Defaults to \"bottom-start\". */\n placement?: Placement;\n /** Positioning strategy. Defaults to \"absolute\". */\n strategy?: Strategy;\n /** Offset distance in pixels. Defaults to 4. */\n offset?: number;\n /** Whether to flip when there's not enough space. Defaults to true. */\n flip?: boolean;\n /** Whether to shift to stay in viewport. Defaults to true. */\n shift?: boolean;\n /** Padding from viewport edge in pixels. Defaults to 8. */\n padding?: number;\n /** Whether to match the width of the reference element. Defaults to false. */\n matchWidth?: boolean;\n}\n\n/**\n * Positions a floating element relative to a reference element.\n *\n * Uses @floating-ui/dom for positioning with flip, shift, and offset\n * middleware. Returns a cleanup function that stops auto-updating.\n *\n * Used by: Select, Popover, Tooltip, Menu, Combobox.\n */\nexport function setupFloating(\n reference: HTMLElement,\n floating: HTMLElement,\n opts: FloatingOptions = {},\n): () => void {\n const {\n placement = \"bottom-start\",\n strategy = \"absolute\",\n offset = 4,\n flip: enableFlip = true,\n shift: enableShift = true,\n padding = 8,\n matchWidth = false,\n } = opts;\n\n const middleware = [\n offsetMiddleware(offset),\n ...(enableFlip ? [flip({ padding })] : []),\n ...(enableShift ? [shift({ padding })] : []),\n ...(matchWidth\n ? [\n size({\n apply({ rects }) {\n Object.assign(floating.style, {\n width: `${rects.reference.width}px`,\n });\n },\n }),\n ]\n : []),\n ];\n\n floating.style.position = strategy;\n\n const cleanup = autoUpdate(reference, floating, async () => {\n const result = await computePosition(reference, floating, {\n placement,\n strategy,\n middleware,\n });\n\n Object.assign(floating.style, {\n left: `${result.x}px`,\n top: `${result.y}px`,\n });\n\n // Set data-side for CSS styling (e.g., arrow direction)\n const side = result.placement.split(\"-\")[0];\n floating.setAttribute(\"data-side\", side);\n });\n\n return cleanup;\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,OAGK;AA2BA,SAAS,cACd,WACA,UACA,OAAwB,CAAC,GACb;AACZ,QAAM;AAAA,IACJ,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,SAAS;AAAA,IACT,MAAM,aAAa;AAAA,IACnB,OAAO,cAAc;AAAA,IACrB,UAAU;AAAA,IACV,aAAa;AAAA,EACf,IAAI;AAEJ,QAAM,aAAa;AAAA,IACjB,iBAAiB,MAAM;AAAA,IACvB,GAAI,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC;AAAA,IACxC,GAAI,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC;AAAA,IAC1C,GAAI,aACA;AAAA,MACE,KAAK;AAAA,QACH,MAAM,EAAE,MAAM,GAAG;AACf,iBAAO,OAAO,SAAS,OAAO;AAAA,YAC5B,OAAO,GAAG,MAAM,UAAU,KAAK;AAAA,UACjC,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH,IACA,CAAC;AAAA,EACP;AAEA,WAAS,MAAM,WAAW;AAE1B,QAAM,UAAU,WAAW,WAAW,UAAU,YAAY;AAC1D,UAAM,SAAS,MAAM,gBAAgB,WAAW,UAAU;AAAA,MACxD;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO,OAAO,SAAS,OAAO;AAAA,MAC5B,MAAM,GAAG,OAAO,CAAC;AAAA,MACjB,KAAK,GAAG,OAAO,CAAC;AAAA,IAClB,CAAC;AAGD,UAAM,OAAO,OAAO,UAAU,MAAM,GAAG,EAAE,CAAC;AAC1C,aAAS,aAAa,aAAa,IAAI;AAAA,EACzC,CAAC;AAED,SAAO;AACT;","names":[]}
@@ -0,0 +1,39 @@
1
+ // src/scroll-lock.ts
2
+ var ScrollLock = class _ScrollLock {
3
+ static count = 0;
4
+ static originalStyles = null;
5
+ /** Lock body scroll. Safe to call multiple times (reference counted). */
6
+ static activate() {
7
+ _ScrollLock.count++;
8
+ if (_ScrollLock.count > 1) return;
9
+ const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
10
+ _ScrollLock.originalStyles = {
11
+ overflow: document.body.style.overflow,
12
+ paddingRight: document.body.style.paddingRight
13
+ };
14
+ document.body.style.overflow = "hidden";
15
+ if (scrollBarWidth > 0) {
16
+ document.body.style.paddingRight = `${scrollBarWidth}px`;
17
+ }
18
+ }
19
+ /** Unlock body scroll. Only actually unlocks when the last caller releases. */
20
+ static deactivate() {
21
+ _ScrollLock.count = Math.max(0, _ScrollLock.count - 1);
22
+ if (_ScrollLock.count > 0) return;
23
+ if (_ScrollLock.originalStyles) {
24
+ document.body.style.overflow = _ScrollLock.originalStyles.overflow;
25
+ document.body.style.paddingRight = _ScrollLock.originalStyles.paddingRight;
26
+ _ScrollLock.originalStyles = null;
27
+ }
28
+ }
29
+ /** Reset internal state. Useful for testing. */
30
+ static reset() {
31
+ _ScrollLock.count = 0;
32
+ _ScrollLock.originalStyles = null;
33
+ }
34
+ };
35
+
36
+ export {
37
+ ScrollLock
38
+ };
39
+ //# sourceMappingURL=chunk-QFU7X4UD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scroll-lock.ts"],"sourcesContent":["/**\n * Prevents body scrolling while a modal is open.\n *\n * Uses a reference count so nested modals don't unlock prematurely.\n * Compensates for the scrollbar width to prevent layout shift.\n */\nexport class ScrollLock {\n private static count = 0;\n private static originalStyles: {\n overflow: string;\n paddingRight: string;\n } | null = null;\n\n /** Lock body scroll. Safe to call multiple times (reference counted). */\n static activate() {\n ScrollLock.count++;\n if (ScrollLock.count > 1) return;\n\n const scrollBarWidth =\n window.innerWidth - document.documentElement.clientWidth;\n\n ScrollLock.originalStyles = {\n overflow: document.body.style.overflow,\n paddingRight: document.body.style.paddingRight,\n };\n\n document.body.style.overflow = \"hidden\";\n if (scrollBarWidth > 0) {\n document.body.style.paddingRight = `${scrollBarWidth}px`;\n }\n }\n\n /** Unlock body scroll. Only actually unlocks when the last caller releases. */\n static deactivate() {\n ScrollLock.count = Math.max(0, ScrollLock.count - 1);\n if (ScrollLock.count > 0) return;\n\n if (ScrollLock.originalStyles) {\n document.body.style.overflow = ScrollLock.originalStyles.overflow;\n document.body.style.paddingRight = ScrollLock.originalStyles.paddingRight;\n ScrollLock.originalStyles = null;\n }\n }\n\n /** Reset internal state. Useful for testing. */\n static reset() {\n ScrollLock.count = 0;\n ScrollLock.originalStyles = null;\n }\n}\n"],"mappings":";AAMO,IAAM,aAAN,MAAM,YAAW;AAAA,EACtB,OAAe,QAAQ;AAAA,EACvB,OAAe,iBAGJ;AAAA;AAAA,EAGX,OAAO,WAAW;AAChB,gBAAW;AACX,QAAI,YAAW,QAAQ,EAAG;AAE1B,UAAM,iBACJ,OAAO,aAAa,SAAS,gBAAgB;AAE/C,gBAAW,iBAAiB;AAAA,MAC1B,UAAU,SAAS,KAAK,MAAM;AAAA,MAC9B,cAAc,SAAS,KAAK,MAAM;AAAA,IACpC;AAEA,aAAS,KAAK,MAAM,WAAW;AAC/B,QAAI,iBAAiB,GAAG;AACtB,eAAS,KAAK,MAAM,eAAe,GAAG,cAAc;AAAA,IACtD;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,aAAa;AAClB,gBAAW,QAAQ,KAAK,IAAI,GAAG,YAAW,QAAQ,CAAC;AACnD,QAAI,YAAW,QAAQ,EAAG;AAE1B,QAAI,YAAW,gBAAgB;AAC7B,eAAS,KAAK,MAAM,WAAW,YAAW,eAAe;AACzD,eAAS,KAAK,MAAM,eAAe,YAAW,eAAe;AAC7D,kBAAW,iBAAiB;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,QAAQ;AACb,gBAAW,QAAQ;AACnB,gBAAW,iBAAiB;AAAA,EAC9B;AACF;","names":[]}
@@ -0,0 +1,31 @@
1
+ interface CollapsibleOptions {
2
+ /** Called when state changes. */
3
+ onToggle?: (open: boolean) => void;
4
+ }
5
+ /**
6
+ * Manages the expand/collapse state of a trigger + content pair.
7
+ *
8
+ * Handles:
9
+ * - `aria-expanded` on trigger
10
+ * - `aria-controls` / `aria-labelledby` linkage
11
+ * - `hidden` attribute on content
12
+ * - `data-state` on both trigger and content
13
+ * - Click handler on trigger
14
+ *
15
+ * Used directly for standalone collapsible sections, and composed
16
+ * by Accordion which adds coordination (single/multiple mode) and
17
+ * keyboard navigation between triggers.
18
+ */
19
+ declare class Collapsible {
20
+ readonly trigger: HTMLElement;
21
+ readonly content: HTMLElement;
22
+ private _open;
23
+ private opts;
24
+ constructor(trigger: HTMLElement, content: HTMLElement, opts?: CollapsibleOptions);
25
+ get isOpen(): boolean;
26
+ toggle(): void;
27
+ expand(): void;
28
+ collapse(): void;
29
+ }
30
+
31
+ export { Collapsible, type CollapsibleOptions };
@@ -0,0 +1,59 @@
1
+ // src/collapsible.ts
2
+ var idCounter = 0;
3
+ function uniqueId(prefix) {
4
+ return `${prefix}-${++idCounter}`;
5
+ }
6
+ var Collapsible = class {
7
+ trigger;
8
+ content;
9
+ _open = false;
10
+ opts;
11
+ constructor(trigger, content, opts = {}) {
12
+ this.trigger = trigger;
13
+ this.content = content;
14
+ this.opts = opts;
15
+ const id = uniqueId("base-collapsible");
16
+ if (!trigger.id) trigger.id = `${id}-trigger`;
17
+ if (!content.id) content.id = `${id}-content`;
18
+ trigger.setAttribute("aria-controls", content.id);
19
+ content.setAttribute("aria-labelledby", trigger.id);
20
+ content.setAttribute("role", "region");
21
+ trigger.setAttribute("aria-expanded", "false");
22
+ trigger.addEventListener("click", () => {
23
+ if (trigger.hasAttribute("disabled") || trigger.getAttribute("aria-disabled") === "true") {
24
+ return;
25
+ }
26
+ this.toggle();
27
+ });
28
+ }
29
+ get isOpen() {
30
+ return this._open;
31
+ }
32
+ toggle() {
33
+ if (this._open) {
34
+ this.collapse();
35
+ } else {
36
+ this.expand();
37
+ }
38
+ }
39
+ expand() {
40
+ if (this._open) return;
41
+ this._open = true;
42
+ this.trigger.setAttribute("aria-expanded", "true");
43
+ this.content.hidden = false;
44
+ this.content.setAttribute("data-state", "open");
45
+ this.opts.onToggle?.(true);
46
+ }
47
+ collapse() {
48
+ if (!this._open) return;
49
+ this._open = false;
50
+ this.trigger.setAttribute("aria-expanded", "false");
51
+ this.content.hidden = true;
52
+ this.content.removeAttribute("data-state");
53
+ this.opts.onToggle?.(false);
54
+ }
55
+ };
56
+ export {
57
+ Collapsible
58
+ };
59
+ //# sourceMappingURL=collapsible.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/collapsible.ts"],"sourcesContent":["let idCounter = 0;\nfunction uniqueId(prefix: string) {\n return `${prefix}-${++idCounter}`;\n}\n\nexport interface CollapsibleOptions {\n /** Called when state changes. */\n onToggle?: (open: boolean) => void;\n}\n\n/**\n * Manages the expand/collapse state of a trigger + content pair.\n *\n * Handles:\n * - `aria-expanded` on trigger\n * - `aria-controls` / `aria-labelledby` linkage\n * - `hidden` attribute on content\n * - `data-state` on both trigger and content\n * - Click handler on trigger\n *\n * Used directly for standalone collapsible sections, and composed\n * by Accordion which adds coordination (single/multiple mode) and\n * keyboard navigation between triggers.\n */\nexport class Collapsible {\n readonly trigger: HTMLElement;\n readonly content: HTMLElement;\n private _open = false;\n private opts: CollapsibleOptions;\n\n constructor(\n trigger: HTMLElement,\n content: HTMLElement,\n opts: CollapsibleOptions = {},\n ) {\n this.trigger = trigger;\n this.content = content;\n this.opts = opts;\n\n // Generate IDs for ARIA linkage if missing\n const id = uniqueId(\"base-collapsible\");\n if (!trigger.id) trigger.id = `${id}-trigger`;\n if (!content.id) content.id = `${id}-content`;\n\n trigger.setAttribute(\"aria-controls\", content.id);\n content.setAttribute(\"aria-labelledby\", trigger.id);\n content.setAttribute(\"role\", \"region\");\n trigger.setAttribute(\"aria-expanded\", \"false\");\n\n // Click handler\n trigger.addEventListener(\"click\", () => {\n if (\n trigger.hasAttribute(\"disabled\") ||\n trigger.getAttribute(\"aria-disabled\") === \"true\"\n ) {\n return;\n }\n this.toggle();\n });\n }\n\n get isOpen() {\n return this._open;\n }\n\n toggle() {\n if (this._open) {\n this.collapse();\n } else {\n this.expand();\n }\n }\n\n expand() {\n if (this._open) return;\n this._open = true;\n this.trigger.setAttribute(\"aria-expanded\", \"true\");\n this.content.hidden = false;\n this.content.setAttribute(\"data-state\", \"open\");\n this.opts.onToggle?.(true);\n }\n\n collapse() {\n if (!this._open) return;\n this._open = false;\n this.trigger.setAttribute(\"aria-expanded\", \"false\");\n this.content.hidden = true;\n this.content.removeAttribute(\"data-state\");\n this.opts.onToggle?.(false);\n }\n}\n"],"mappings":";AAAA,IAAI,YAAY;AAChB,SAAS,SAAS,QAAgB;AAChC,SAAO,GAAG,MAAM,IAAI,EAAE,SAAS;AACjC;AAqBO,IAAM,cAAN,MAAkB;AAAA,EACd;AAAA,EACA;AAAA,EACD,QAAQ;AAAA,EACR;AAAA,EAER,YACE,SACA,SACA,OAA2B,CAAC,GAC5B;AACA,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,OAAO;AAGZ,UAAM,KAAK,SAAS,kBAAkB;AACtC,QAAI,CAAC,QAAQ,GAAI,SAAQ,KAAK,GAAG,EAAE;AACnC,QAAI,CAAC,QAAQ,GAAI,SAAQ,KAAK,GAAG,EAAE;AAEnC,YAAQ,aAAa,iBAAiB,QAAQ,EAAE;AAChD,YAAQ,aAAa,mBAAmB,QAAQ,EAAE;AAClD,YAAQ,aAAa,QAAQ,QAAQ;AACrC,YAAQ,aAAa,iBAAiB,OAAO;AAG7C,YAAQ,iBAAiB,SAAS,MAAM;AACtC,UACE,QAAQ,aAAa,UAAU,KAC/B,QAAQ,aAAa,eAAe,MAAM,QAC1C;AACA;AAAA,MACF;AACA,WAAK,OAAO;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS;AACP,QAAI,KAAK,OAAO;AACd,WAAK,SAAS;AAAA,IAChB,OAAO;AACL,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAAA,EAEA,SAAS;AACP,QAAI,KAAK,MAAO;AAChB,SAAK,QAAQ;AACb,SAAK,QAAQ,aAAa,iBAAiB,MAAM;AACjD,SAAK,QAAQ,SAAS;AACtB,SAAK,QAAQ,aAAa,cAAc,MAAM;AAC9C,SAAK,KAAK,WAAW,IAAI;AAAA,EAC3B;AAAA,EAEA,WAAW;AACT,QAAI,CAAC,KAAK,MAAO;AACjB,SAAK,QAAQ;AACb,SAAK,QAAQ,aAAa,iBAAiB,OAAO;AAClD,SAAK,QAAQ,SAAS;AACtB,SAAK,QAAQ,gBAAgB,YAAY;AACzC,SAAK,KAAK,WAAW,KAAK;AAAA,EAC5B;AACF;","names":[]}
@@ -0,0 +1,37 @@
1
+ type Orientation = "horizontal" | "vertical" | "both";
2
+ interface CompositeOptions {
3
+ /** Which arrow keys navigate. Defaults to "horizontal". */
4
+ orientation?: Orientation;
5
+ /** Whether navigation wraps from end to start. Defaults to true. */
6
+ loop?: boolean;
7
+ /** Whether Home/End keys are handled. Defaults to true. */
8
+ homeEnd?: boolean;
9
+ /** Called when the highlighted index changes. */
10
+ onHighlightedIndexChange?: (index: number) => void;
11
+ }
12
+ /**
13
+ * Manages roving tabindex keyboard navigation for a list of items.
14
+ *
15
+ * Ported from Base UI's composite navigation pattern. Handles arrow keys,
16
+ * Home/End, disabled item skipping, orientation, RTL, and loop wrapping.
17
+ *
18
+ * Used by: Tabs, Menu, Select, RadioGroup, ToggleGroup.
19
+ */
20
+ declare class CompositeNavigation {
21
+ private container;
22
+ private itemSelector;
23
+ private opts;
24
+ private keydownHandler;
25
+ private active;
26
+ constructor(container: HTMLElement, itemSelector: string, opts?: CompositeOptions);
27
+ private getItems;
28
+ private isForwardKey;
29
+ private isBackwardKey;
30
+ activate(): void;
31
+ deactivate(): void;
32
+ /** Set the roving tabindex so only the given index is tabbable. */
33
+ setActiveIndex(items: HTMLElement[], index: number): void;
34
+ isActive(): boolean;
35
+ }
36
+
37
+ export { CompositeNavigation, type CompositeOptions, type Orientation };
@@ -0,0 +1,7 @@
1
+ import {
2
+ CompositeNavigation
3
+ } from "./chunk-LN2MVY72.js";
4
+ export {
5
+ CompositeNavigation
6
+ };
7
+ //# sourceMappingURL=composite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,38 @@
1
+ type DismissReason = "escape-key" | "outside-press";
2
+ interface DismissOptions {
3
+ /** Called when the user dismisses the element. */
4
+ onDismiss: (reason: DismissReason) => void;
5
+ /** Whether pressing Escape dismisses. Defaults to true. */
6
+ escapeKey?: boolean;
7
+ /** Whether clicking outside dismisses. Can be a predicate. Defaults to true. */
8
+ outsidePress?: boolean | ((event: PointerEvent) => boolean);
9
+ }
10
+ /**
11
+ * Handles dismissal of floating/overlay elements via Escape key
12
+ * and outside pointer press.
13
+ *
14
+ * Uses a global stack so that only the topmost (most recently opened)
15
+ * controller handles Escape. This prevents nested dialogs from all
16
+ * closing on a single keypress.
17
+ *
18
+ * Delays the outside-press listener by one animation frame to prevent
19
+ * the opening click from immediately triggering a dismiss.
20
+ */
21
+ declare class DismissController {
22
+ private root;
23
+ /** @internal */ opts: {
24
+ onDismiss: (reason: DismissReason) => void;
25
+ escapeKey: boolean;
26
+ outsidePress: boolean | ((event: PointerEvent) => boolean);
27
+ };
28
+ private outsideHandler;
29
+ private active;
30
+ private frameId;
31
+ /** @internal */ escapePrevented: boolean;
32
+ constructor(root: HTMLElement, opts: DismissOptions);
33
+ activate(): void;
34
+ deactivate(): void;
35
+ isActive(): boolean;
36
+ }
37
+
38
+ export { DismissController, type DismissOptions, type DismissReason };
@@ -0,0 +1,7 @@
1
+ import {
2
+ DismissController
3
+ } from "./chunk-4CMGFJKB.js";
4
+ export {
5
+ DismissController
6
+ };
7
+ //# sourceMappingURL=dismiss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,29 @@
1
+ import { Placement, Strategy } from '@floating-ui/dom';
2
+
3
+ interface FloatingOptions {
4
+ /** Placement relative to the reference element. Defaults to "bottom-start". */
5
+ placement?: Placement;
6
+ /** Positioning strategy. Defaults to "absolute". */
7
+ strategy?: Strategy;
8
+ /** Offset distance in pixels. Defaults to 4. */
9
+ offset?: number;
10
+ /** Whether to flip when there's not enough space. Defaults to true. */
11
+ flip?: boolean;
12
+ /** Whether to shift to stay in viewport. Defaults to true. */
13
+ shift?: boolean;
14
+ /** Padding from viewport edge in pixels. Defaults to 8. */
15
+ padding?: number;
16
+ /** Whether to match the width of the reference element. Defaults to false. */
17
+ matchWidth?: boolean;
18
+ }
19
+ /**
20
+ * Positions a floating element relative to a reference element.
21
+ *
22
+ * Uses @floating-ui/dom for positioning with flip, shift, and offset
23
+ * middleware. Returns a cleanup function that stops auto-updating.
24
+ *
25
+ * Used by: Select, Popover, Tooltip, Menu, Combobox.
26
+ */
27
+ declare function setupFloating(reference: HTMLElement, floating: HTMLElement, opts?: FloatingOptions): () => void;
28
+
29
+ export { type FloatingOptions, setupFloating };
@@ -0,0 +1,7 @@
1
+ import {
2
+ setupFloating
3
+ } from "./chunk-Q2HX4RL6.js";
4
+ export {
5
+ setupFloating
6
+ };
7
+ //# sourceMappingURL=floating.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,26 @@
1
+ interface FocusTrapOptions {
2
+ /** Where to place focus when the trap activates. Defaults to "first". */
3
+ initialFocus?: HTMLElement | "first" | "container";
4
+ /** Whether to return focus to the previously focused element on deactivate. Defaults to true. */
5
+ returnFocus?: boolean;
6
+ }
7
+ /**
8
+ * Traps keyboard focus within a container element.
9
+ *
10
+ * Re-queries tabbable elements on every Tab press so dynamically
11
+ * added/removed elements are handled correctly. Uses the `tabbable`
12
+ * library for accurate focusable-element detection.
13
+ */
14
+ declare class FocusTrap {
15
+ private root;
16
+ private opts;
17
+ private previouslyFocused;
18
+ private keydownHandler;
19
+ private active;
20
+ constructor(root: HTMLElement, opts?: FocusTrapOptions);
21
+ activate(): void;
22
+ deactivate(): void;
23
+ isActive(): boolean;
24
+ }
25
+
26
+ export { FocusTrap, type FocusTrapOptions };
@@ -0,0 +1,7 @@
1
+ import {
2
+ FocusTrap
3
+ } from "./chunk-M46BYUQY.js";
4
+ export {
5
+ FocusTrap
6
+ };
7
+ //# sourceMappingURL=focus-trap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,10 @@
1
+ export { FocusTrap, FocusTrapOptions } from './focus-trap.js';
2
+ export { ScrollLock } from './scroll-lock.js';
3
+ export { DismissController, DismissOptions } from './dismiss.js';
4
+ export { markOthersInert } from './inert.js';
5
+ export { createPortal } from './portal.js';
6
+ export { mergeProps } from './merge-props.js';
7
+ export { CompositeNavigation, CompositeOptions, Orientation } from './composite.js';
8
+ export { FloatingOptions, setupFloating } from './floating.js';
9
+ export { Typeahead, TypeaheadOptions } from './typeahead.js';
10
+ import '@floating-ui/dom';
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ import {
2
+ createPortal
3
+ } from "./chunk-HN6LBLTZ.js";
4
+ import {
5
+ ScrollLock
6
+ } from "./chunk-QFU7X4UD.js";
7
+ import {
8
+ Typeahead
9
+ } from "./chunk-OKZYYVLM.js";
10
+ import {
11
+ CompositeNavigation
12
+ } from "./chunk-LN2MVY72.js";
13
+ import {
14
+ DismissController
15
+ } from "./chunk-4CMGFJKB.js";
16
+ import {
17
+ setupFloating
18
+ } from "./chunk-Q2HX4RL6.js";
19
+ import {
20
+ FocusTrap
21
+ } from "./chunk-M46BYUQY.js";
22
+ import {
23
+ markOthersInert
24
+ } from "./chunk-N2FDXX6C.js";
25
+ import {
26
+ mergeProps
27
+ } from "./chunk-I6QJ4ATJ.js";
28
+ export {
29
+ CompositeNavigation,
30
+ DismissController,
31
+ FocusTrap,
32
+ ScrollLock,
33
+ Typeahead,
34
+ createPortal,
35
+ markOthersInert,
36
+ mergeProps,
37
+ setupFloating
38
+ };
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Marks all elements outside the given element as `aria-hidden="true"`,
3
+ * making only the target element visible to screen readers.
4
+ *
5
+ * Returns a cleanup function that restores the original `aria-hidden`
6
+ * values. Handles elements that already had `aria-hidden` set.
7
+ */
8
+ declare function markOthersInert(element: HTMLElement): () => void;
9
+
10
+ export { markOthersInert };
package/dist/inert.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ markOthersInert
3
+ } from "./chunk-N2FDXX6C.js";
4
+ export {
5
+ markOthersInert
6
+ };
7
+ //# sourceMappingURL=inert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,13 @@
1
+ type AnyProps = Record<string, unknown>;
2
+ /**
3
+ * Merges multiple prop objects, composing event handlers and
4
+ * concatenating class names.
5
+ *
6
+ * - Event handlers (`on*`): all handlers run; latest set first.
7
+ * - `class` / `className`: concatenated with spaces.
8
+ * - `style` (object): shallow-merged, later values win.
9
+ * - All other props: later values win.
10
+ */
11
+ declare function mergeProps<T extends AnyProps>(...propSets: Array<T | undefined>): T;
12
+
13
+ export { mergeProps };
@@ -0,0 +1,7 @@
1
+ import {
2
+ mergeProps
3
+ } from "./chunk-I6QJ4ATJ.js";
4
+ export {
5
+ mergeProps
6
+ };
7
+ //# sourceMappingURL=merge-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Moves an element to the end of `document.body` inside a portal container.
3
+ *
4
+ * Returns a cleanup function that removes the portal container and
5
+ * unregisters the View Transitions cleanup listener.
6
+ */
7
+ declare function createPortal(content: HTMLElement, id: string): () => void;
8
+
9
+ export { createPortal };
package/dist/portal.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ createPortal
3
+ } from "./chunk-HN6LBLTZ.js";
4
+ export {
5
+ createPortal
6
+ };
7
+ //# sourceMappingURL=portal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Prevents body scrolling while a modal is open.
3
+ *
4
+ * Uses a reference count so nested modals don't unlock prematurely.
5
+ * Compensates for the scrollbar width to prevent layout shift.
6
+ */
7
+ declare class ScrollLock {
8
+ private static count;
9
+ private static originalStyles;
10
+ /** Lock body scroll. Safe to call multiple times (reference counted). */
11
+ static activate(): void;
12
+ /** Unlock body scroll. Only actually unlocks when the last caller releases. */
13
+ static deactivate(): void;
14
+ /** Reset internal state. Useful for testing. */
15
+ static reset(): void;
16
+ }
17
+
18
+ export { ScrollLock };
@@ -0,0 +1,7 @@
1
+ import {
2
+ ScrollLock
3
+ } from "./chunk-QFU7X4UD.js";
4
+ export {
5
+ ScrollLock
6
+ };
7
+ //# sourceMappingURL=scroll-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,35 @@
1
+ interface TypeaheadOptions {
2
+ /** Called when a match is found. Receives the matching index. */
3
+ onMatch: (index: number) => void;
4
+ /** Returns the text content for the item at the given index. */
5
+ getItemText: (index: number) => string;
6
+ /** Total number of items. */
7
+ getItemCount: () => number;
8
+ /** Timeout in ms before the search buffer resets. Defaults to 1000. */
9
+ timeout?: number;
10
+ }
11
+ /**
12
+ * Handles type-to-select behavior for lists.
13
+ *
14
+ * Accumulates typed characters into a buffer. On each keystroke, searches
15
+ * for an item whose text starts with the buffer (case-insensitive).
16
+ * Resets the buffer after a configurable timeout of inactivity.
17
+ *
18
+ * Used by: Select, Menu, Combobox, Listbox.
19
+ */
20
+ declare class Typeahead {
21
+ private opts;
22
+ private buffer;
23
+ private timerId;
24
+ private timeout;
25
+ constructor(opts: TypeaheadOptions);
26
+ /**
27
+ * Handle a keydown event. Call this from your component's keydown handler.
28
+ * Returns true if the key was consumed (a printable character), false otherwise.
29
+ */
30
+ handle(key: string): boolean;
31
+ /** Reset the buffer immediately. */
32
+ reset(): void;
33
+ }
34
+
35
+ export { Typeahead, type TypeaheadOptions };
@@ -0,0 +1,7 @@
1
+ import {
2
+ Typeahead
3
+ } from "./chunk-OKZYYVLM.js";
4
+ export {
5
+ Typeahead
6
+ };
7
+ //# sourceMappingURL=typeahead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@dogsbay/primitives",
3
+ "version": "0.2.0-beta.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./focus-trap": {
13
+ "types": "./dist/focus-trap.d.ts",
14
+ "import": "./dist/focus-trap.js"
15
+ },
16
+ "./scroll-lock": {
17
+ "types": "./dist/scroll-lock.d.ts",
18
+ "import": "./dist/scroll-lock.js"
19
+ },
20
+ "./dismiss": {
21
+ "types": "./dist/dismiss.d.ts",
22
+ "import": "./dist/dismiss.js"
23
+ },
24
+ "./inert": {
25
+ "types": "./dist/inert.d.ts",
26
+ "import": "./dist/inert.js"
27
+ },
28
+ "./portal": {
29
+ "types": "./dist/portal.d.ts",
30
+ "import": "./dist/portal.js"
31
+ },
32
+ "./merge-props": {
33
+ "types": "./dist/merge-props.d.ts",
34
+ "import": "./dist/merge-props.js"
35
+ },
36
+ "./composite": {
37
+ "types": "./dist/composite.d.ts",
38
+ "import": "./dist/composite.js"
39
+ },
40
+ "./floating": {
41
+ "types": "./dist/floating.d.ts",
42
+ "import": "./dist/floating.js"
43
+ },
44
+ "./typeahead": {
45
+ "types": "./dist/typeahead.d.ts",
46
+ "import": "./dist/typeahead.js"
47
+ },
48
+ "./collapsible": {
49
+ "types": "./dist/collapsible.d.ts",
50
+ "import": "./dist/collapsible.js"
51
+ }
52
+ },
53
+ "files": [
54
+ "dist"
55
+ ],
56
+ "dependencies": {
57
+ "@floating-ui/dom": "^1.7.6",
58
+ "tabbable": "^6.2.0"
59
+ },
60
+ "devDependencies": {
61
+ "@vitest/browser": "^3.1.0",
62
+ "happy-dom": "^20.8.9",
63
+ "playwright": "^1.50.0",
64
+ "tsup": "^8.4.0",
65
+ "typescript": "^5.8.0",
66
+ "vitest": "^3.1.0"
67
+ },
68
+ "license": "MIT",
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "https://github.com/dogsbay/dogsbay.git",
72
+ "directory": "packages/primitives"
73
+ },
74
+ "homepage": "https://github.com/dogsbay/dogsbay/tree/main/packages/primitives",
75
+ "bugs": {
76
+ "url": "https://github.com/dogsbay/dogsbay/issues"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "dev": "tsup --watch",
81
+ "test": "vitest run",
82
+ "test:watch": "vitest",
83
+ "typecheck": "tsc --noEmit"
84
+ }
85
+ }