@human-kit/svelte-components 1.0.0-alpha.1

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 (140) hide show
  1. package/dist/combobox/TODO.md +175 -0
  2. package/dist/combobox/button/combobox-button.svelte +57 -0
  3. package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
  4. package/dist/combobox/index.d.ts +14 -0
  5. package/dist/combobox/index.js +18 -0
  6. package/dist/combobox/index.parts.d.ts +10 -0
  7. package/dist/combobox/index.parts.js +11 -0
  8. package/dist/combobox/input/combobox-input.svelte +98 -0
  9. package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
  10. package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
  11. package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
  12. package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
  13. package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
  14. package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
  15. package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
  16. package/dist/combobox/list/combobox-listbox.svelte +76 -0
  17. package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
  18. package/dist/combobox/popover/combobox-popover.svelte +69 -0
  19. package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
  20. package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
  21. package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
  22. package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
  23. package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
  24. package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
  25. package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
  26. package/dist/combobox/root/combobox-test.svelte +43 -0
  27. package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
  28. package/dist/combobox/root/combobox.svelte +696 -0
  29. package/dist/combobox/root/combobox.svelte.d.ts +58 -0
  30. package/dist/combobox/root/context.d.ts +90 -0
  31. package/dist/combobox/root/context.js +15 -0
  32. package/dist/combobox/tag/combobox-tag.svelte +58 -0
  33. package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
  34. package/dist/combobox/tag/tag-context-provider.svelte +36 -0
  35. package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
  36. package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
  37. package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
  38. package/dist/combobox/tags/combobox-tags.svelte +50 -0
  39. package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
  40. package/dist/dialog/content/dialog-content.svelte +121 -0
  41. package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
  42. package/dist/dialog/index.d.ts +10 -0
  43. package/dist/dialog/index.js +15 -0
  44. package/dist/dialog/index.parts.d.ts +5 -0
  45. package/dist/dialog/index.parts.js +6 -0
  46. package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
  47. package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
  48. package/dist/dialog/portal/dialog-portal.svelte +32 -0
  49. package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
  50. package/dist/dialog/root/context.d.ts +25 -0
  51. package/dist/dialog/root/context.js +8 -0
  52. package/dist/dialog/root/dialog-root.svelte +99 -0
  53. package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
  54. package/dist/dialog/root/dialog-stack.d.ts +32 -0
  55. package/dist/dialog/root/dialog-stack.js +55 -0
  56. package/dist/dialog/root/dialog-test.svelte +38 -0
  57. package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
  58. package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
  59. package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
  60. package/dist/dialog/root/nested-dialog-test.svelte +63 -0
  61. package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
  62. package/dist/dialog/root/types.d.ts +10 -0
  63. package/dist/dialog/root/types.js +1 -0
  64. package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
  65. package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
  66. package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
  67. package/dist/hooks/use-virtual-focus.svelte.js +201 -0
  68. package/dist/index.d.ts +13 -0
  69. package/dist/index.js +19 -0
  70. package/dist/input/index.d.ts +3 -0
  71. package/dist/input/index.js +3 -0
  72. package/dist/input/input.svelte +19 -0
  73. package/dist/input/input.svelte.d.ts +8 -0
  74. package/dist/label/index.d.ts +3 -0
  75. package/dist/label/index.js +3 -0
  76. package/dist/label/label.svelte +21 -0
  77. package/dist/label/label.svelte.d.ts +8 -0
  78. package/dist/listbox/index.d.ts +6 -0
  79. package/dist/listbox/index.js +10 -0
  80. package/dist/listbox/index.parts.d.ts +2 -0
  81. package/dist/listbox/index.parts.js +3 -0
  82. package/dist/listbox/item/listbox-item.svelte +186 -0
  83. package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
  84. package/dist/listbox/root/context.d.ts +73 -0
  85. package/dist/listbox/root/context.js +249 -0
  86. package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
  87. package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
  88. package/dist/listbox/root/listbox-test.svelte +27 -0
  89. package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
  90. package/dist/listbox/root/listbox.svelte +146 -0
  91. package/dist/listbox/root/listbox.svelte.d.ts +54 -0
  92. package/dist/popover/content/popover-content-test.svelte +43 -0
  93. package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
  94. package/dist/popover/content/popover-content.svelte +167 -0
  95. package/dist/popover/content/popover-content.svelte.d.ts +38 -0
  96. package/dist/popover/index.d.ts +8 -0
  97. package/dist/popover/index.js +14 -0
  98. package/dist/popover/index.parts.d.ts +4 -0
  99. package/dist/popover/index.parts.js +5 -0
  100. package/dist/popover/root/context.d.ts +24 -0
  101. package/dist/popover/root/context.js +10 -0
  102. package/dist/popover/root/popover-root.svelte +87 -0
  103. package/dist/popover/root/popover-root.svelte.d.ts +20 -0
  104. package/dist/popover/root/popover-test.svelte +40 -0
  105. package/dist/popover/root/popover-test.svelte.d.ts +11 -0
  106. package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
  107. package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
  108. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
  109. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
  110. package/dist/popover/trigger/popover-trigger.svelte +71 -0
  111. package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
  112. package/dist/portal/index.d.ts +1 -0
  113. package/dist/portal/index.js +1 -0
  114. package/dist/portal/portal.svelte +44 -0
  115. package/dist/portal/portal.svelte.d.ts +10 -0
  116. package/dist/primitives/aria-hide-outside.d.ts +38 -0
  117. package/dist/primitives/aria-hide-outside.js +152 -0
  118. package/dist/primitives/click-outside.d.ts +26 -0
  119. package/dist/primitives/click-outside.js +66 -0
  120. package/dist/primitives/floating.d.ts +57 -0
  121. package/dist/primitives/floating.js +179 -0
  122. package/dist/primitives/focus-trap.d.ts +19 -0
  123. package/dist/primitives/focus-trap.js +102 -0
  124. package/dist/primitives/index.d.ts +6 -0
  125. package/dist/primitives/index.js +7 -0
  126. package/dist/primitives/keyboard-navigation.d.ts +88 -0
  127. package/dist/primitives/keyboard-navigation.js +274 -0
  128. package/dist/primitives/scroll-lock.d.ts +19 -0
  129. package/dist/primitives/scroll-lock.js +62 -0
  130. package/dist/test-mocks/app-environment.d.ts +7 -0
  131. package/dist/test-mocks/app-environment.js +7 -0
  132. package/dist/test-mocks/app-navigation.d.ts +11 -0
  133. package/dist/test-mocks/app-navigation.js +11 -0
  134. package/dist/test-mocks/app-stores.d.ts +16 -0
  135. package/dist/test-mocks/app-stores.js +18 -0
  136. package/dist/utils/cn.d.ts +2 -0
  137. package/dist/utils/cn.js +5 -0
  138. package/dist/utils/index.d.ts +1 -0
  139. package/dist/utils/index.js +1 -0
  140. package/package.json +99 -0
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { onMount, onDestroy, tick } from 'svelte';
4
+ import { browser } from '$app/environment';
5
+
6
+ type PortalProps = {
7
+ /** Target element or selector to render into. Defaults to document.body */
8
+ target?: string;
9
+ /** Content to render in the portal */
10
+ children?: Snippet;
11
+ };
12
+
13
+ let { target = 'body', children }: PortalProps = $props();
14
+
15
+ let wrapper: HTMLDivElement | undefined = $state();
16
+
17
+ onMount(async () => {
18
+ if (!browser || !wrapper) return;
19
+
20
+ await tick();
21
+
22
+ const targetEl = document.querySelector(target);
23
+ if (!targetEl) {
24
+ console.error(`Portal: target "${target}" not found`);
25
+ return;
26
+ }
27
+
28
+ targetEl.appendChild(wrapper);
29
+ });
30
+
31
+ onDestroy(() => {
32
+ // Wrapper will be automatically removed when component is destroyed
33
+ // because Svelte still controls it
34
+ if (wrapper && wrapper.parentNode) {
35
+ wrapper.parentNode.removeChild(wrapper);
36
+ }
37
+ });
38
+ </script>
39
+
40
+ <div bind:this={wrapper} style="display: contents;">
41
+ {#if children}
42
+ {@render children()}
43
+ {/if}
44
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ type PortalProps = {
3
+ /** Target element or selector to render into. Defaults to document.body */
4
+ target?: string;
5
+ /** Content to render in the portal */
6
+ children?: Snippet;
7
+ };
8
+ declare const Portal: import("svelte").Component<PortalProps, {}, "">;
9
+ type Portal = ReturnType<typeof Portal>;
10
+ export default Portal;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Hides all elements in the DOM tree outside of the given targets from screen readers
3
+ * and makes them inert. Based on React Aria's ariaHideOutside implementation.
4
+ *
5
+ * This works by walking the DOM from the body and marking all siblings
6
+ * of ancestors of the target elements as inert.
7
+ */
8
+ interface HideOutsideResult {
9
+ /** Call this to restore the original state */
10
+ restore: () => void;
11
+ }
12
+ /**
13
+ * Hides all content outside of the target elements from assistive technologies
14
+ * and makes it non-interactive.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const { restore } = hideOutside([popoverRef]);
19
+ * // Later, when popover closes:
20
+ * restore();
21
+ * ```
22
+ */
23
+ export declare function hideOutside(targets: HTMLElement[]): HideOutsideResult;
24
+ /**
25
+ * Svelte action that hides all content outside of the element.
26
+ *
27
+ * @example
28
+ * ```svelte
29
+ * <div use:ariaHideOutside={enabled}>
30
+ * Modal content
31
+ * </div>
32
+ * ```
33
+ */
34
+ export declare function ariaHideOutside(node: HTMLElement, enabled?: boolean): {
35
+ update(newEnabled: boolean): void;
36
+ destroy(): void;
37
+ };
38
+ export {};
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Hides all elements in the DOM tree outside of the given targets from screen readers
3
+ * and makes them inert. Based on React Aria's ariaHideOutside implementation.
4
+ *
5
+ * This works by walking the DOM from the body and marking all siblings
6
+ * of ancestors of the target elements as inert.
7
+ */
8
+ /**
9
+ * Global hidden state tracker.
10
+ * Allows multiple overlapping hideOutside calls without restoring too early.
11
+ */
12
+ const hiddenState = new Map();
13
+ function hideElement(element) {
14
+ const existing = hiddenState.get(element);
15
+ if (existing) {
16
+ existing.count += 1;
17
+ hiddenState.set(element, existing);
18
+ }
19
+ else {
20
+ hiddenState.set(element, {
21
+ count: 1,
22
+ hadInert: element.hasAttribute('inert'),
23
+ ariaHidden: element.getAttribute('aria-hidden')
24
+ });
25
+ }
26
+ element.setAttribute('inert', '');
27
+ element.setAttribute('aria-hidden', 'true');
28
+ }
29
+ function restoreElement(element) {
30
+ const existing = hiddenState.get(element);
31
+ if (!existing)
32
+ return;
33
+ if (existing.count > 1) {
34
+ existing.count -= 1;
35
+ hiddenState.set(element, existing);
36
+ return;
37
+ }
38
+ if (!existing.hadInert) {
39
+ element.removeAttribute('inert');
40
+ }
41
+ else {
42
+ element.setAttribute('inert', '');
43
+ }
44
+ if (existing.ariaHidden === null) {
45
+ element.removeAttribute('aria-hidden');
46
+ }
47
+ else {
48
+ element.setAttribute('aria-hidden', existing.ariaHidden);
49
+ }
50
+ hiddenState.delete(element);
51
+ }
52
+ /**
53
+ * Hides all content outside of the target elements from assistive technologies
54
+ * and makes it non-interactive.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const { restore } = hideOutside([popoverRef]);
59
+ * // Later, when popover closes:
60
+ * restore();
61
+ * ```
62
+ */
63
+ export function hideOutside(targets) {
64
+ const affectedElements = new Set();
65
+ const targetSet = new Set(targets);
66
+ const targetAncestors = new Set();
67
+ for (const target of targets) {
68
+ let current = target.parentElement;
69
+ while (current) {
70
+ targetAncestors.add(current);
71
+ current = current.parentElement;
72
+ }
73
+ }
74
+ function walk(root) {
75
+ const children = root.children;
76
+ for (let i = 0; i < children.length; i++) {
77
+ const child = children[i];
78
+ const tagName = child.tagName;
79
+ if (tagName === 'SCRIPT' || tagName === 'STYLE' || tagName === 'LINK') {
80
+ continue;
81
+ }
82
+ if (targetSet.has(child)) {
83
+ continue;
84
+ }
85
+ if (targetAncestors.has(child)) {
86
+ walk(child);
87
+ }
88
+ else {
89
+ hideElement(child);
90
+ affectedElements.add(child);
91
+ }
92
+ }
93
+ }
94
+ if (document.body) {
95
+ walk(document.body);
96
+ }
97
+ let restored = false;
98
+ return {
99
+ restore() {
100
+ if (restored)
101
+ return;
102
+ restored = true;
103
+ affectedElements.forEach((element) => {
104
+ restoreElement(element);
105
+ });
106
+ affectedElements.clear();
107
+ }
108
+ };
109
+ }
110
+ /**
111
+ * Svelte action that hides all content outside of the element.
112
+ *
113
+ * @example
114
+ * ```svelte
115
+ * <div use:ariaHideOutside={enabled}>
116
+ * Modal content
117
+ * </div>
118
+ * ```
119
+ */
120
+ export function ariaHideOutside(node, enabled = true) {
121
+ let result = null;
122
+ function activate() {
123
+ requestAnimationFrame(() => {
124
+ if (node.isConnected) {
125
+ result = hideOutside([node]);
126
+ }
127
+ });
128
+ }
129
+ function deactivate() {
130
+ if (result) {
131
+ result.restore();
132
+ result = null;
133
+ }
134
+ }
135
+ if (enabled) {
136
+ activate();
137
+ }
138
+ return {
139
+ update(newEnabled) {
140
+ if (newEnabled && !enabled) {
141
+ activate();
142
+ }
143
+ else if (!newEnabled && enabled) {
144
+ deactivate();
145
+ }
146
+ enabled = newEnabled;
147
+ },
148
+ destroy() {
149
+ deactivate();
150
+ }
151
+ };
152
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Click outside primitive.
3
+ * Detects clicks outside of an element.
4
+ */
5
+ export type ClickOutsideOptions = {
6
+ /** Callback when clicking outside. */
7
+ handler: () => void;
8
+ /** Whether the listener is enabled. */
9
+ enabled?: boolean;
10
+ /** Elements to ignore (clicks on these won't trigger). */
11
+ ignore?: (HTMLElement | null)[];
12
+ };
13
+ /**
14
+ * Svelte action that detects clicks outside of an element.
15
+ *
16
+ * @example
17
+ * ```svelte
18
+ * <div use:clickOutside={{ handler: close, ignore: [triggerRef] }}>
19
+ * Popover content
20
+ * </div>
21
+ * ```
22
+ */
23
+ export declare function clickOutside(node: HTMLElement, options: ClickOutsideOptions): {
24
+ update(newOptions: ClickOutsideOptions): void;
25
+ destroy(): void;
26
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Click outside primitive.
3
+ * Detects clicks outside of an element.
4
+ */
5
+ /**
6
+ * Check if an element is in a "top layer" (portal, dialog, popover, etc.)
7
+ * that was spawned from within the reference node.
8
+ * This prevents clicks on nested portals from triggering clickOutside.
9
+ */
10
+ function isInTopLayer(target) {
11
+ if (!(target instanceof Element))
12
+ return false;
13
+ // Check if the element or any ancestor is marked as top-layer
14
+ // This includes our popovers, nested dialogs, and other portaled content
15
+ const topLayerElement = target.closest('[data-dialog-content], [role="dialog"]');
16
+ return topLayerElement !== null;
17
+ }
18
+ /**
19
+ * Svelte action that detects clicks outside of an element.
20
+ *
21
+ * @example
22
+ * ```svelte
23
+ * <div use:clickOutside={{ handler: close, ignore: [triggerRef] }}>
24
+ * Popover content
25
+ * </div>
26
+ * ```
27
+ */
28
+ export function clickOutside(node, options) {
29
+ let { handler, enabled = true, ignore = [] } = options;
30
+ function handleClick(event) {
31
+ if (!enabled)
32
+ return;
33
+ const target = event.target;
34
+ if (node.contains(target))
35
+ return;
36
+ for (const el of ignore) {
37
+ if (el && el.contains(target))
38
+ return;
39
+ }
40
+ // Don't trigger if clicking on a top-layer element (portal content)
41
+ // This prevents closing when clicking on nested popovers/dialogs
42
+ if (isInTopLayer(target))
43
+ return;
44
+ handler();
45
+ }
46
+ if (enabled) {
47
+ document.addEventListener('mousedown', handleClick, true);
48
+ }
49
+ return {
50
+ update(newOptions) {
51
+ const wasEnabled = enabled;
52
+ handler = newOptions.handler;
53
+ enabled = newOptions.enabled ?? true;
54
+ ignore = newOptions.ignore ?? [];
55
+ if (enabled && !wasEnabled) {
56
+ document.addEventListener('mousedown', handleClick, true);
57
+ }
58
+ else if (!enabled && wasEnabled) {
59
+ document.removeEventListener('mousedown', handleClick, true);
60
+ }
61
+ },
62
+ destroy() {
63
+ document.removeEventListener('mousedown', handleClick, true);
64
+ }
65
+ };
66
+ }
@@ -0,0 +1,57 @@
1
+ import { type Placement as FloatingPlacement } from '@floating-ui/dom';
2
+ /**
3
+ * Placement options for floating elements.
4
+ * Follows the specification with logical 'start'/'end' values.
5
+ */
6
+ export type Placement = 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end';
7
+ /**
8
+ * Extended placement type that includes human-readable variants.
9
+ */
10
+ export type ExtendedPlacement = Placement | 'bottom left' | 'bottom right' | 'top left' | 'top right' | 'left top' | 'left bottom' | 'right top' | 'right bottom' | 'start' | 'start top' | 'start bottom' | 'end' | 'end top' | 'end bottom';
11
+ /**
12
+ * Options for the floating element positioning.
13
+ */
14
+ export type FloatingOptions = {
15
+ /** Offset along the main axis from the anchor. */
16
+ offset?: number;
17
+ /** Placement relative to the anchor element. */
18
+ placement?: ExtendedPlacement;
19
+ /** Whether to flip when there's insufficient space. */
20
+ shouldFlip?: boolean;
21
+ /** Boundary element for positioning constraints. */
22
+ boundaryElement?: Element | null;
23
+ /** Callback when position is updated. */
24
+ onPositionUpdate?: (x: number, y: number, placement: FloatingPlacement) => void;
25
+ };
26
+ /**
27
+ * Creates a Svelte action for positioning a floating element relative to an anchor.
28
+ *
29
+ * Exposes CSS custom properties on the floating element:
30
+ * - `--trigger-width`: The trigger element's width
31
+ * - `--trigger-height`: The trigger element's height
32
+ * - `--available-width`: Available width between trigger and viewport edge
33
+ * - `--available-height`: Available height between trigger and viewport edge
34
+ * - `--transform-origin`: Coordinates for animations (e.g., "center top")
35
+ */
36
+ export declare function createFloating(anchorElement: HTMLElement | null, options?: FloatingOptions): (floatingElement: HTMLElement) => {
37
+ destroy(): void;
38
+ } | undefined;
39
+ /**
40
+ * Simple Svelte action for floating positioning.
41
+ * Use when you just need positioning without complex state management.
42
+ *
43
+ * Exposes CSS custom properties on the floating element:
44
+ * - `--trigger-width`: The trigger element's width
45
+ * - `--trigger-height`: The trigger element's height
46
+ * - `--available-width`: Available width between trigger and viewport edge
47
+ * - `--available-height`: Available height between trigger and viewport edge
48
+ * - `--transform-origin`: Coordinates for animations (e.g., "center top")
49
+ */
50
+ export declare function floating(floatingElement: HTMLElement, options: {
51
+ anchor: HTMLElement | null;
52
+ } & FloatingOptions): {
53
+ update(newOptions: {
54
+ anchor: HTMLElement | null;
55
+ } & FloatingOptions): void;
56
+ destroy(): void;
57
+ } | undefined;
@@ -0,0 +1,179 @@
1
+ import { computePosition, flip, shift, offset as offsetMiddleware, size, autoUpdate } from '@floating-ui/dom';
2
+ /**
3
+ * Converts extended placement syntax to Floating UI placement.
4
+ */
5
+ function normalizeExtendedPlacement(placement) {
6
+ const mappings = {
7
+ bottom: 'bottom',
8
+ 'bottom-start': 'bottom-start',
9
+ 'bottom-end': 'bottom-end',
10
+ 'bottom left': 'bottom-start',
11
+ 'bottom right': 'bottom-end',
12
+ top: 'top',
13
+ 'top-start': 'top-start',
14
+ 'top-end': 'top-end',
15
+ 'top left': 'top-start',
16
+ 'top right': 'top-end',
17
+ left: 'left',
18
+ 'left-start': 'left-start',
19
+ 'left-end': 'left-end',
20
+ 'left top': 'left-start',
21
+ 'left bottom': 'left-end',
22
+ right: 'right',
23
+ 'right-start': 'right-start',
24
+ 'right-end': 'right-end',
25
+ 'right top': 'right-start',
26
+ 'right bottom': 'right-end',
27
+ start: 'left',
28
+ 'start top': 'left-start',
29
+ 'start bottom': 'left-end',
30
+ end: 'right',
31
+ 'end top': 'right-start',
32
+ 'end bottom': 'right-end'
33
+ };
34
+ return mappings[placement] || 'bottom';
35
+ }
36
+ /**
37
+ * Calculates the transform origin based on the final placement.
38
+ * Used for animations that should originate from the anchor point.
39
+ */
40
+ function getTransformOrigin(placement) {
41
+ const [side, align] = placement.split('-');
42
+ const sideMap = {
43
+ top: 'bottom',
44
+ bottom: 'top',
45
+ left: 'right',
46
+ right: 'left'
47
+ };
48
+ const alignMap = {
49
+ start: 'left',
50
+ end: 'right'
51
+ };
52
+ const vertical = side === 'top' || side === 'bottom';
53
+ const oppositeSide = sideMap[side] || 'top';
54
+ if (vertical) {
55
+ const horizontalAlign = align ? alignMap[align] || 'center' : 'center';
56
+ return `${horizontalAlign} ${oppositeSide}`;
57
+ }
58
+ else {
59
+ const verticalAlign = align ? (align === 'start' ? 'top' : 'bottom') : 'center';
60
+ return `${oppositeSide} ${verticalAlign}`;
61
+ }
62
+ }
63
+ /**
64
+ * Creates a Svelte action for positioning a floating element relative to an anchor.
65
+ *
66
+ * Exposes CSS custom properties on the floating element:
67
+ * - `--trigger-width`: The trigger element's width
68
+ * - `--trigger-height`: The trigger element's height
69
+ * - `--available-width`: Available width between trigger and viewport edge
70
+ * - `--available-height`: Available height between trigger and viewport edge
71
+ * - `--transform-origin`: Coordinates for animations (e.g., "center top")
72
+ */
73
+ export function createFloating(anchorElement, options = {}) {
74
+ const { offset = 8, placement = 'bottom', shouldFlip = true, boundaryElement = null, onPositionUpdate } = options;
75
+ let cleanup = null;
76
+ function action(floatingElement) {
77
+ if (!anchorElement)
78
+ return;
79
+ const normalizedPlacement = normalizeExtendedPlacement(placement);
80
+ const middleware = [
81
+ offsetMiddleware(offset),
82
+ ...(shouldFlip ? [flip({ boundary: boundaryElement || undefined })] : []),
83
+ shift({ boundary: boundaryElement || undefined }),
84
+ size({
85
+ apply({ rects, availableWidth, availableHeight, elements, placement }) {
86
+ const floatingEl = elements.floating;
87
+ floatingEl.style.setProperty('--trigger-width', `${rects.reference.width}px`);
88
+ floatingEl.style.setProperty('--trigger-height', `${rects.reference.height}px`);
89
+ floatingEl.style.setProperty('--available-width', `${availableWidth}px`);
90
+ floatingEl.style.setProperty('--available-height', `${availableHeight}px`);
91
+ floatingEl.style.setProperty('--transform-origin', getTransformOrigin(placement));
92
+ }
93
+ })
94
+ ];
95
+ async function updatePosition() {
96
+ if (!anchorElement || !floatingElement)
97
+ return;
98
+ const { x, y, placement: finalPlacement } = await computePosition(anchorElement, floatingElement, {
99
+ placement: normalizedPlacement,
100
+ middleware
101
+ });
102
+ Object.assign(floatingElement.style, {
103
+ left: `${x}px`,
104
+ top: `${y}px`
105
+ });
106
+ onPositionUpdate?.(x, y, finalPlacement);
107
+ }
108
+ cleanup = autoUpdate(anchorElement, floatingElement, updatePosition);
109
+ return {
110
+ destroy() {
111
+ cleanup?.();
112
+ cleanup = null;
113
+ }
114
+ };
115
+ }
116
+ return action;
117
+ }
118
+ /**
119
+ * Simple Svelte action for floating positioning.
120
+ * Use when you just need positioning without complex state management.
121
+ *
122
+ * Exposes CSS custom properties on the floating element:
123
+ * - `--trigger-width`: The trigger element's width
124
+ * - `--trigger-height`: The trigger element's height
125
+ * - `--available-width`: Available width between trigger and viewport edge
126
+ * - `--available-height`: Available height between trigger and viewport edge
127
+ * - `--transform-origin`: Coordinates for animations (e.g., "center top")
128
+ */
129
+ export function floating(floatingElement, options) {
130
+ const { anchor, ...floatingOptions } = options;
131
+ if (!anchor)
132
+ return;
133
+ const normalizedPlacement = normalizeExtendedPlacement(floatingOptions.placement || 'bottom');
134
+ const offset = floatingOptions.offset ?? 8;
135
+ const shouldFlip = floatingOptions.shouldFlip ?? true;
136
+ const boundaryElement = floatingOptions.boundaryElement || null;
137
+ const middleware = [
138
+ offsetMiddleware(offset),
139
+ ...(shouldFlip ? [flip({ boundary: boundaryElement || undefined })] : []),
140
+ shift({ boundary: boundaryElement || undefined }),
141
+ size({
142
+ apply({ rects, availableWidth, availableHeight, elements, placement }) {
143
+ const floatingEl = elements.floating;
144
+ floatingEl.style.setProperty('--trigger-width', `${rects.reference.width}px`);
145
+ floatingEl.style.setProperty('--trigger-height', `${rects.reference.height}px`);
146
+ floatingEl.style.setProperty('--available-width', `${availableWidth}px`);
147
+ floatingEl.style.setProperty('--available-height', `${availableHeight}px`);
148
+ floatingEl.style.setProperty('--transform-origin', getTransformOrigin(placement));
149
+ }
150
+ })
151
+ ];
152
+ let cleanup = null;
153
+ async function updatePosition() {
154
+ if (!anchor || !floatingElement)
155
+ return;
156
+ const { x, y, placement: finalPlacement } = await computePosition(anchor, floatingElement, {
157
+ placement: normalizedPlacement,
158
+ middleware,
159
+ strategy: 'fixed' // Use fixed strategy for portal-rendered elements
160
+ });
161
+ Object.assign(floatingElement.style, {
162
+ left: `${x}px`,
163
+ top: `${y}px`
164
+ });
165
+ floatingOptions.onPositionUpdate?.(x, y, finalPlacement);
166
+ }
167
+ cleanup = autoUpdate(anchor, floatingElement, updatePosition);
168
+ return {
169
+ update(newOptions) {
170
+ cleanup?.();
171
+ if (newOptions.anchor) {
172
+ cleanup = autoUpdate(newOptions.anchor, floatingElement, updatePosition);
173
+ }
174
+ },
175
+ destroy() {
176
+ cleanup?.();
177
+ }
178
+ };
179
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Focus trap primitive.
3
+ * Traps keyboard focus within a container element.
4
+ */
5
+ /**
6
+ * Svelte action that traps focus within an element.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <div use:focusTrap={isOpen}>
11
+ * <button>First</button>
12
+ * <button>Last</button>
13
+ * </div>
14
+ * ```
15
+ */
16
+ export declare function focusTrap(node: HTMLElement, enabled?: boolean): {
17
+ update(newEnabled: boolean): void;
18
+ destroy(): void;
19
+ };