@classic-homes/theme-svelte 0.1.52 → 0.1.54
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.
- package/dist/__tests__/a11y/Alert.a11y.test.js +84 -0
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +126 -0
- package/dist/__tests__/a11y/Button.a11y.test.js +66 -0
- package/dist/__tests__/a11y/Card.a11y.test.js +77 -0
- package/dist/__tests__/a11y/Combobox.a11y.test.js +209 -0
- package/dist/__tests__/a11y/Dialog.a11y.test.js +103 -0
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +104 -0
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +158 -0
- package/dist/__tests__/a11y/Icon.a11y.test.js +119 -0
- package/dist/__tests__/a11y/Input.a11y.test.js +96 -0
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +102 -0
- package/dist/__tests__/a11y/Switch.a11y.test.js +95 -0
- package/dist/__tests__/a11y/Tabs.a11y.test.js +97 -0
- package/dist/__tests__/a11y/Textarea.a11y.test.js +111 -0
- package/dist/__tests__/setup.js +6 -0
- package/dist/lib/components/Alert.svelte +1 -1
- package/dist/lib/components/AlertDialog.svelte +1 -1
- package/dist/lib/components/AlertDialog.svelte.d.ts +2 -2
- package/dist/lib/components/Badge.svelte +49 -12
- package/dist/lib/components/Badge.svelte.d.ts +9 -0
- package/dist/lib/components/Button.svelte +14 -2
- package/dist/lib/components/CardTitle.svelte +9 -3
- package/dist/lib/components/CardTitle.svelte.d.ts +2 -0
- package/dist/lib/components/Combobox.svelte +19 -2
- package/dist/lib/components/DataPanelHeader.svelte +4 -4
- package/dist/lib/components/DataPanelTab.svelte +1 -1
- package/dist/lib/components/DataTable.svelte +7 -7
- package/dist/lib/components/DataTable.svelte.d.ts +5 -5
- package/dist/lib/components/DateTimePicker.svelte +7 -4
- package/dist/lib/components/Dialog.svelte +6 -6
- package/dist/lib/components/DropdownMenu.svelte +14 -10
- package/dist/lib/components/FileUpload.svelte +5 -10
- package/dist/lib/components/FormField.svelte +7 -1
- package/dist/lib/components/HeaderSearch.svelte +9 -9
- package/dist/lib/components/Icon.svelte +5 -5
- package/dist/lib/components/Input.svelte +32 -0
- package/dist/lib/components/Input.svelte.d.ts +8 -0
- package/dist/lib/components/LiveRegion.svelte +61 -0
- package/dist/lib/components/LiveRegion.svelte.d.ts +18 -0
- package/dist/lib/components/LoadingOverlay.svelte +148 -0
- package/dist/lib/components/LoadingOverlay.svelte.d.ts +29 -0
- package/dist/lib/components/LoadingPage.svelte +86 -0
- package/dist/lib/components/LoadingPage.svelte.d.ts +20 -0
- package/dist/lib/components/LoadingSection.svelte +54 -0
- package/dist/lib/components/LoadingSection.svelte.d.ts +25 -0
- package/dist/lib/components/MultiSelect.svelte +23 -2
- package/dist/lib/components/NumberInput.svelte +4 -4
- package/dist/lib/components/OTPInput.svelte +14 -0
- package/dist/lib/components/PasswordInput.svelte +14 -1
- package/dist/lib/components/PasswordInput.svelte.d.ts +4 -0
- package/dist/lib/components/ProgressBar.svelte +91 -0
- package/dist/lib/components/ProgressBar.svelte.d.ts +18 -0
- package/dist/lib/components/RadioGroup.svelte +5 -5
- package/dist/lib/components/Select.svelte +13 -1
- package/dist/lib/components/Signature.svelte +5 -5
- package/dist/lib/components/Skeleton.svelte +13 -3
- package/dist/lib/components/Skeleton.svelte.d.ts +2 -0
- package/dist/lib/components/SkipLink.svelte +67 -0
- package/dist/lib/components/SkipLink.svelte.d.ts +25 -0
- package/dist/lib/components/Slider.svelte +6 -2
- package/dist/lib/components/Switch.svelte +8 -0
- package/dist/lib/components/Switch.svelte.d.ts +4 -0
- package/dist/lib/components/Tabs.svelte +15 -5
- package/dist/lib/components/Tabs.svelte.d.ts +6 -0
- package/dist/lib/components/Textarea.svelte +19 -0
- package/dist/lib/components/Textarea.svelte.d.ts +8 -0
- package/dist/lib/components/Toast.svelte +1 -1
- package/dist/lib/components/layout/AuthLayout.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +1 -1
- package/dist/lib/components/layout/Footer.svelte +1 -1
- package/dist/lib/components/layout/PublicLayout.svelte +2 -2
- package/dist/lib/components/layout/QuickLinks.svelte +3 -3
- package/dist/lib/components/layout/Sidebar.svelte +1 -1
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +1 -2
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +20 -21
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +2 -2
- package/dist/lib/components/notifications/NotificationFilters.svelte +1 -1
- package/dist/lib/components/notifications/NotificationItem.svelte +2 -1
- package/dist/lib/composables/index.d.ts +8 -0
- package/dist/lib/composables/index.js +9 -0
- package/dist/lib/composables/useAnnounce.svelte.d.ts +65 -0
- package/dist/lib/composables/useAnnounce.svelte.js +76 -0
- package/dist/lib/composables/useArrowNavigation.svelte.d.ts +42 -0
- package/dist/lib/composables/useArrowNavigation.svelte.js +138 -0
- package/dist/lib/composables/useClickOutside.svelte.d.ts +88 -0
- package/dist/lib/composables/useClickOutside.svelte.js +100 -0
- package/dist/lib/composables/useEscapeKey.svelte.d.ts +48 -0
- package/dist/lib/composables/useEscapeKey.svelte.js +50 -0
- package/dist/lib/composables/useFocusRef.svelte.d.ts +46 -0
- package/dist/lib/composables/useFocusRef.svelte.js +92 -0
- package/dist/lib/composables/useFocusTrap.svelte.d.ts +42 -0
- package/dist/lib/composables/useFocusTrap.svelte.js +136 -0
- package/dist/lib/composables/useReducedMotion.svelte.d.ts +47 -0
- package/dist/lib/composables/useReducedMotion.svelte.js +67 -0
- package/dist/lib/composables/useRovingTabindex.svelte.d.ts +54 -0
- package/dist/lib/composables/useRovingTabindex.svelte.js +128 -0
- package/dist/lib/index.d.ts +7 -1
- package/dist/lib/index.js +11 -1
- package/dist/lib/stores/theme.svelte.d.ts +1 -1
- package/dist/lib/utils.d.ts +4 -2
- package/dist/lib/utils.js +5 -11
- package/package.json +11 -5
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a focus action for use with Svelte's use: directive.
|
|
3
|
+
*
|
|
4
|
+
* Useful for focusing form fields, dialog close buttons, or other
|
|
5
|
+
* elements that should receive focus when they appear.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { useFocusRef } from '@classic-homes/theme-svelte';
|
|
11
|
+
*
|
|
12
|
+
* const { action } = useFocusRef({ focusOnMount: true });
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* <input use:action type="search" />
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example With delay (for animations)
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <script>
|
|
21
|
+
* import { useFocusRef } from '@classic-homes/theme-svelte';
|
|
22
|
+
*
|
|
23
|
+
* const { action } = useFocusRef({ focusOnMount: true, delay: 200 });
|
|
24
|
+
* </script>
|
|
25
|
+
*
|
|
26
|
+
* <button use:action>Close</button>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useFocusRef(options = {}) {
|
|
30
|
+
let focusOnMount = $state(options.focusOnMount ?? false);
|
|
31
|
+
let preventScroll = $state(options.preventScroll ?? false);
|
|
32
|
+
let delay = $state(options.delay ?? 0);
|
|
33
|
+
let element = null;
|
|
34
|
+
let timeoutId = null;
|
|
35
|
+
let frameId = null;
|
|
36
|
+
function focus() {
|
|
37
|
+
if (element && typeof element.focus === 'function') {
|
|
38
|
+
element.focus({ preventScroll });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function scheduleFocus() {
|
|
42
|
+
if (!focusOnMount)
|
|
43
|
+
return;
|
|
44
|
+
if (delay > 0) {
|
|
45
|
+
timeoutId = setTimeout(focus, delay);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Use requestAnimationFrame to ensure element is rendered
|
|
49
|
+
frameId = requestAnimationFrame(focus);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function cleanup() {
|
|
53
|
+
if (timeoutId !== null) {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
timeoutId = null;
|
|
56
|
+
}
|
|
57
|
+
if (frameId !== null) {
|
|
58
|
+
cancelAnimationFrame(frameId);
|
|
59
|
+
frameId = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Svelte action to apply auto-focus to an element
|
|
64
|
+
*/
|
|
65
|
+
function action(node) {
|
|
66
|
+
element = node;
|
|
67
|
+
scheduleFocus();
|
|
68
|
+
return {
|
|
69
|
+
update(newOptions) {
|
|
70
|
+
cleanup();
|
|
71
|
+
focusOnMount = newOptions.focusOnMount ?? false;
|
|
72
|
+
preventScroll = newOptions.preventScroll ?? false;
|
|
73
|
+
delay = newOptions.delay ?? 0;
|
|
74
|
+
scheduleFocus();
|
|
75
|
+
},
|
|
76
|
+
destroy() {
|
|
77
|
+
cleanup();
|
|
78
|
+
element = null;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Programmatically focus the element
|
|
84
|
+
*/
|
|
85
|
+
function focusNow() {
|
|
86
|
+
focus();
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
action,
|
|
90
|
+
focus: focusNow,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface UseFocusTrapOptions {
|
|
2
|
+
/** Whether the focus trap is active */
|
|
3
|
+
active?: boolean;
|
|
4
|
+
/** Element to return focus to when trap is deactivated */
|
|
5
|
+
returnFocusTo?: HTMLElement | null;
|
|
6
|
+
/** Selector for the element to focus initially (defaults to first focusable) */
|
|
7
|
+
initialFocus?: string;
|
|
8
|
+
/** Whether to prevent scrolling when focusing elements */
|
|
9
|
+
preventScroll?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a focus trap action for use with Svelte's use: directive.
|
|
13
|
+
*
|
|
14
|
+
* Traps focus within the container so Tab/Shift+Tab cycles through
|
|
15
|
+
* focusable elements without escaping. Useful for modals, dialogs,
|
|
16
|
+
* and dropdown menus.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <script>
|
|
21
|
+
* import { useFocusTrap } from '@classic-homes/theme-svelte';
|
|
22
|
+
*
|
|
23
|
+
* const { action } = useFocusTrap({ active: isOpen });
|
|
24
|
+
* </script>
|
|
25
|
+
*
|
|
26
|
+
* {#if isOpen}
|
|
27
|
+
* <div use:action role="dialog">
|
|
28
|
+
* <button>First</button>
|
|
29
|
+
* <button>Second</button>
|
|
30
|
+
* <button onclick={() => isOpen = false}>Close</button>
|
|
31
|
+
* </div>
|
|
32
|
+
* {/if}
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function useFocusTrap(options?: UseFocusTrapOptions): {
|
|
36
|
+
action: (node: HTMLElement) => {
|
|
37
|
+
update(newOptions: UseFocusTrapOptions): void;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
};
|
|
40
|
+
activate: () => void;
|
|
41
|
+
deactivate: () => void;
|
|
42
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const FOCUSABLE_SELECTOR = [
|
|
2
|
+
'a[href]',
|
|
3
|
+
'area[href]',
|
|
4
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
5
|
+
'select:not([disabled])',
|
|
6
|
+
'textarea:not([disabled])',
|
|
7
|
+
'button:not([disabled])',
|
|
8
|
+
'iframe',
|
|
9
|
+
'object',
|
|
10
|
+
'embed',
|
|
11
|
+
'[contenteditable]',
|
|
12
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
13
|
+
].join(',');
|
|
14
|
+
function getFocusableElements(container) {
|
|
15
|
+
const elements = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
16
|
+
return Array.from(elements).filter((el) => el.offsetParent !== null && getComputedStyle(el).visibility !== 'hidden');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a focus trap action for use with Svelte's use: directive.
|
|
20
|
+
*
|
|
21
|
+
* Traps focus within the container so Tab/Shift+Tab cycles through
|
|
22
|
+
* focusable elements without escaping. Useful for modals, dialogs,
|
|
23
|
+
* and dropdown menus.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```svelte
|
|
27
|
+
* <script>
|
|
28
|
+
* import { useFocusTrap } from '@classic-homes/theme-svelte';
|
|
29
|
+
*
|
|
30
|
+
* const { action } = useFocusTrap({ active: isOpen });
|
|
31
|
+
* </script>
|
|
32
|
+
*
|
|
33
|
+
* {#if isOpen}
|
|
34
|
+
* <div use:action role="dialog">
|
|
35
|
+
* <button>First</button>
|
|
36
|
+
* <button>Second</button>
|
|
37
|
+
* <button onclick={() => isOpen = false}>Close</button>
|
|
38
|
+
* </div>
|
|
39
|
+
* {/if}
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function useFocusTrap(options = {}) {
|
|
43
|
+
let active = $state(options.active ?? true);
|
|
44
|
+
let returnFocusTo = $state(options.returnFocusTo ?? null);
|
|
45
|
+
let initialFocus = $state(options.initialFocus);
|
|
46
|
+
let preventScroll = $state(options.preventScroll ?? false);
|
|
47
|
+
let container = null;
|
|
48
|
+
let previousActiveElement = null;
|
|
49
|
+
function handleKeyDown(event) {
|
|
50
|
+
if (!active || event.key !== 'Tab' || !container)
|
|
51
|
+
return;
|
|
52
|
+
const focusableElements = getFocusableElements(container);
|
|
53
|
+
if (focusableElements.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const firstElement = focusableElements[0];
|
|
56
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
57
|
+
if (event.shiftKey) {
|
|
58
|
+
// Shift + Tab: if on first element, move to last
|
|
59
|
+
if (document.activeElement === firstElement) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
lastElement.focus({ preventScroll });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Tab: if on last element, move to first
|
|
66
|
+
if (document.activeElement === lastElement) {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
firstElement.focus({ preventScroll });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function activate() {
|
|
73
|
+
if (!container)
|
|
74
|
+
return;
|
|
75
|
+
// Save current active element to return focus later
|
|
76
|
+
previousActiveElement = document.activeElement;
|
|
77
|
+
// Focus initial element
|
|
78
|
+
const focusableElements = getFocusableElements(container);
|
|
79
|
+
if (focusableElements.length > 0) {
|
|
80
|
+
let elementToFocus = null;
|
|
81
|
+
if (initialFocus) {
|
|
82
|
+
elementToFocus = container.querySelector(initialFocus);
|
|
83
|
+
}
|
|
84
|
+
if (!elementToFocus) {
|
|
85
|
+
elementToFocus = focusableElements[0];
|
|
86
|
+
}
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
elementToFocus?.focus({ preventScroll });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
92
|
+
}
|
|
93
|
+
function deactivate() {
|
|
94
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
95
|
+
// Return focus when trap is deactivated
|
|
96
|
+
const elementToReturn = returnFocusTo ?? previousActiveElement;
|
|
97
|
+
if (elementToReturn && typeof elementToReturn.focus === 'function') {
|
|
98
|
+
elementToReturn.focus({ preventScroll });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Svelte action to apply focus trap to an element
|
|
103
|
+
*/
|
|
104
|
+
function action(node) {
|
|
105
|
+
container = node;
|
|
106
|
+
if (active) {
|
|
107
|
+
activate();
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
update(newOptions) {
|
|
111
|
+
const wasActive = active;
|
|
112
|
+
active = newOptions.active ?? true;
|
|
113
|
+
returnFocusTo = newOptions.returnFocusTo ?? null;
|
|
114
|
+
initialFocus = newOptions.initialFocus;
|
|
115
|
+
preventScroll = newOptions.preventScroll ?? false;
|
|
116
|
+
if (!wasActive && active) {
|
|
117
|
+
activate();
|
|
118
|
+
}
|
|
119
|
+
else if (wasActive && !active) {
|
|
120
|
+
deactivate();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
destroy() {
|
|
124
|
+
if (active) {
|
|
125
|
+
deactivate();
|
|
126
|
+
}
|
|
127
|
+
container = null;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
action,
|
|
133
|
+
activate,
|
|
134
|
+
deactivate,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable to detect user's reduced motion preference from system settings.
|
|
3
|
+
*
|
|
4
|
+
* Responds to the CSS media query `prefers-reduced-motion: reduce` which users
|
|
5
|
+
* can set in their operating system accessibility settings to indicate they
|
|
6
|
+
* prefer minimal motion/animation.
|
|
7
|
+
*
|
|
8
|
+
* Use this composable to disable or reduce animations for users who have this preference,
|
|
9
|
+
* which is important for accessibility (vestibular disorders, motion sensitivity, etc.).
|
|
10
|
+
*
|
|
11
|
+
* @returns Object with reactive `value` property - true if user prefers reduced motion
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { useReducedMotion } from '@classic-homes/theme-svelte';
|
|
17
|
+
*
|
|
18
|
+
* const reducedMotion = useReducedMotion();
|
|
19
|
+
* </script>
|
|
20
|
+
*
|
|
21
|
+
* <div
|
|
22
|
+
* style:transition={$reducedMotion.value ? 'none' : 'transform 0.3s ease'}
|
|
23
|
+
* >
|
|
24
|
+
* Content
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example with svelte/motion
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <script>
|
|
31
|
+
* import { fly } from 'svelte/transition';
|
|
32
|
+
* import { useReducedMotion } from '@classic-homes/theme-svelte';
|
|
33
|
+
*
|
|
34
|
+
* const reducedMotion = useReducedMotion();
|
|
35
|
+
* const duration = $derived(reducedMotion.value ? 0 : 300);
|
|
36
|
+
* </script>
|
|
37
|
+
*
|
|
38
|
+
* <div
|
|
39
|
+
* in:fly={{ y: reducedMotion.value ? 0 : 20, duration }}
|
|
40
|
+
* >
|
|
41
|
+
* Content
|
|
42
|
+
* </div>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function useReducedMotion(): {
|
|
46
|
+
readonly value: boolean;
|
|
47
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable to detect user's reduced motion preference from system settings.
|
|
3
|
+
*
|
|
4
|
+
* Responds to the CSS media query `prefers-reduced-motion: reduce` which users
|
|
5
|
+
* can set in their operating system accessibility settings to indicate they
|
|
6
|
+
* prefer minimal motion/animation.
|
|
7
|
+
*
|
|
8
|
+
* Use this composable to disable or reduce animations for users who have this preference,
|
|
9
|
+
* which is important for accessibility (vestibular disorders, motion sensitivity, etc.).
|
|
10
|
+
*
|
|
11
|
+
* @returns Object with reactive `value` property - true if user prefers reduced motion
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { useReducedMotion } from '@classic-homes/theme-svelte';
|
|
17
|
+
*
|
|
18
|
+
* const reducedMotion = useReducedMotion();
|
|
19
|
+
* </script>
|
|
20
|
+
*
|
|
21
|
+
* <div
|
|
22
|
+
* style:transition={$reducedMotion.value ? 'none' : 'transform 0.3s ease'}
|
|
23
|
+
* >
|
|
24
|
+
* Content
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example with svelte/motion
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <script>
|
|
31
|
+
* import { fly } from 'svelte/transition';
|
|
32
|
+
* import { useReducedMotion } from '@classic-homes/theme-svelte';
|
|
33
|
+
*
|
|
34
|
+
* const reducedMotion = useReducedMotion();
|
|
35
|
+
* const duration = $derived(reducedMotion.value ? 0 : 300);
|
|
36
|
+
* </script>
|
|
37
|
+
*
|
|
38
|
+
* <div
|
|
39
|
+
* in:fly={{ y: reducedMotion.value ? 0 : 20, duration }}
|
|
40
|
+
* >
|
|
41
|
+
* Content
|
|
42
|
+
* </div>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useReducedMotion() {
|
|
46
|
+
let reducedMotion = $state(false);
|
|
47
|
+
$effect(() => {
|
|
48
|
+
if (typeof window === 'undefined')
|
|
49
|
+
return;
|
|
50
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
51
|
+
// Set initial value
|
|
52
|
+
reducedMotion = mediaQuery.matches;
|
|
53
|
+
// Listen for changes
|
|
54
|
+
const handleChange = (event) => {
|
|
55
|
+
reducedMotion = event.matches;
|
|
56
|
+
};
|
|
57
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
58
|
+
return () => {
|
|
59
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
get value() {
|
|
64
|
+
return reducedMotion;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface UseRovingTabindexOptions {
|
|
2
|
+
/** Whether navigation wraps from end to start */
|
|
3
|
+
loop?: boolean;
|
|
4
|
+
/** Selector for focusable items */
|
|
5
|
+
itemSelector?: string;
|
|
6
|
+
/** Initial focused index */
|
|
7
|
+
initialIndex?: number;
|
|
8
|
+
/** Callback when active index changes */
|
|
9
|
+
onIndexChange?: (index: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Composable implementing roving tabindex pattern for composite widgets.
|
|
13
|
+
*
|
|
14
|
+
* Only one item in the group has tabindex="0" at a time, while
|
|
15
|
+
* others have tabindex="-1". Arrow keys move focus within the group.
|
|
16
|
+
*
|
|
17
|
+
* This is the recommended pattern for widgets like toolbars, menus,
|
|
18
|
+
* listboxes, tree views, and tabs.
|
|
19
|
+
*
|
|
20
|
+
* @see https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```svelte
|
|
24
|
+
* <script>
|
|
25
|
+
* import { useRovingTabindex } from '@classic-homes/theme-svelte';
|
|
26
|
+
*
|
|
27
|
+
* const roving = useRovingTabindex();
|
|
28
|
+
* </script>
|
|
29
|
+
*
|
|
30
|
+
* <div role="toolbar" use:roving.action>
|
|
31
|
+
* {#each tools as tool, i}
|
|
32
|
+
* <button
|
|
33
|
+
* data-roving-tabindex-item
|
|
34
|
+
* tabindex={roving.activeIndex === i ? 0 : -1}
|
|
35
|
+
* on:click={() => roving.setActiveIndex(i)}
|
|
36
|
+
* >
|
|
37
|
+
* {tool.label}
|
|
38
|
+
* </button>
|
|
39
|
+
* {/each}
|
|
40
|
+
* </div>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function useRovingTabindex(options?: UseRovingTabindexOptions): {
|
|
44
|
+
action: (node: HTMLElement) => {
|
|
45
|
+
update(newOptions: UseRovingTabindexOptions): void;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
};
|
|
48
|
+
readonly activeIndex: number;
|
|
49
|
+
setActiveIndex: (index: number) => void;
|
|
50
|
+
focusNext: () => void;
|
|
51
|
+
focusPrevious: () => void;
|
|
52
|
+
focusFirst: () => void;
|
|
53
|
+
focusLast: () => void;
|
|
54
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const DEFAULT_SELECTOR = '[data-roving-tabindex-item]';
|
|
2
|
+
/**
|
|
3
|
+
* Composable implementing roving tabindex pattern for composite widgets.
|
|
4
|
+
*
|
|
5
|
+
* Only one item in the group has tabindex="0" at a time, while
|
|
6
|
+
* others have tabindex="-1". Arrow keys move focus within the group.
|
|
7
|
+
*
|
|
8
|
+
* This is the recommended pattern for widgets like toolbars, menus,
|
|
9
|
+
* listboxes, tree views, and tabs.
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { useRovingTabindex } from '@classic-homes/theme-svelte';
|
|
17
|
+
*
|
|
18
|
+
* const roving = useRovingTabindex();
|
|
19
|
+
* </script>
|
|
20
|
+
*
|
|
21
|
+
* <div role="toolbar" use:roving.action>
|
|
22
|
+
* {#each tools as tool, i}
|
|
23
|
+
* <button
|
|
24
|
+
* data-roving-tabindex-item
|
|
25
|
+
* tabindex={roving.activeIndex === i ? 0 : -1}
|
|
26
|
+
* on:click={() => roving.setActiveIndex(i)}
|
|
27
|
+
* >
|
|
28
|
+
* {tool.label}
|
|
29
|
+
* </button>
|
|
30
|
+
* {/each}
|
|
31
|
+
* </div>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function useRovingTabindex(options = {}) {
|
|
35
|
+
let loop = $state(options.loop ?? true);
|
|
36
|
+
let itemSelector = $state(options.itemSelector ?? DEFAULT_SELECTOR);
|
|
37
|
+
let activeIndex = $state(options.initialIndex ?? 0);
|
|
38
|
+
let onIndexChange = $state(options.onIndexChange);
|
|
39
|
+
let container = null;
|
|
40
|
+
function getItems() {
|
|
41
|
+
if (!container)
|
|
42
|
+
return [];
|
|
43
|
+
return Array.from(container.querySelectorAll(itemSelector));
|
|
44
|
+
}
|
|
45
|
+
function focusIndex(index) {
|
|
46
|
+
const items = getItems();
|
|
47
|
+
if (items.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
let targetIndex = index;
|
|
50
|
+
if (loop) {
|
|
51
|
+
if (targetIndex < 0)
|
|
52
|
+
targetIndex = items.length - 1;
|
|
53
|
+
if (targetIndex >= items.length)
|
|
54
|
+
targetIndex = 0;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1));
|
|
58
|
+
}
|
|
59
|
+
activeIndex = targetIndex;
|
|
60
|
+
onIndexChange?.(targetIndex);
|
|
61
|
+
const element = items[targetIndex];
|
|
62
|
+
if (element) {
|
|
63
|
+
element.focus();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function handleKeyDown(event) {
|
|
67
|
+
const items = getItems();
|
|
68
|
+
if (items.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
let handled = false;
|
|
71
|
+
switch (event.key) {
|
|
72
|
+
case 'ArrowUp':
|
|
73
|
+
case 'ArrowLeft':
|
|
74
|
+
focusIndex(activeIndex - 1);
|
|
75
|
+
handled = true;
|
|
76
|
+
break;
|
|
77
|
+
case 'ArrowDown':
|
|
78
|
+
case 'ArrowRight':
|
|
79
|
+
focusIndex(activeIndex + 1);
|
|
80
|
+
handled = true;
|
|
81
|
+
break;
|
|
82
|
+
case 'Home':
|
|
83
|
+
focusIndex(0);
|
|
84
|
+
handled = true;
|
|
85
|
+
break;
|
|
86
|
+
case 'End':
|
|
87
|
+
focusIndex(items.length - 1);
|
|
88
|
+
handled = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (handled) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Svelte action to apply roving tabindex to a container
|
|
98
|
+
*/
|
|
99
|
+
function action(node) {
|
|
100
|
+
container = node;
|
|
101
|
+
node.addEventListener('keydown', handleKeyDown);
|
|
102
|
+
return {
|
|
103
|
+
update(newOptions) {
|
|
104
|
+
loop = newOptions.loop ?? true;
|
|
105
|
+
itemSelector = newOptions.itemSelector ?? DEFAULT_SELECTOR;
|
|
106
|
+
if (newOptions.initialIndex !== undefined && newOptions.initialIndex !== activeIndex) {
|
|
107
|
+
activeIndex = newOptions.initialIndex;
|
|
108
|
+
}
|
|
109
|
+
onIndexChange = newOptions.onIndexChange;
|
|
110
|
+
},
|
|
111
|
+
destroy() {
|
|
112
|
+
node.removeEventListener('keydown', handleKeyDown);
|
|
113
|
+
container = null;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
action,
|
|
119
|
+
get activeIndex() {
|
|
120
|
+
return activeIndex;
|
|
121
|
+
},
|
|
122
|
+
setActiveIndex: focusIndex,
|
|
123
|
+
focusNext: () => focusIndex(activeIndex + 1),
|
|
124
|
+
focusPrevious: () => focusIndex(activeIndex - 1),
|
|
125
|
+
focusFirst: () => focusIndex(0),
|
|
126
|
+
focusLast: () => focusIndex(getItems().length - 1),
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export { default as Spinner } from './components/Spinner.svelte';
|
|
|
29
29
|
export { default as Skeleton } from './components/Skeleton.svelte';
|
|
30
30
|
export { default as Toast } from './components/Toast.svelte';
|
|
31
31
|
export { default as ToastContainer } from './components/ToastContainer.svelte';
|
|
32
|
+
export { default as ProgressBar } from './components/ProgressBar.svelte';
|
|
33
|
+
export { default as LoadingOverlay } from './components/LoadingOverlay.svelte';
|
|
34
|
+
export { default as LoadingPage } from './components/LoadingPage.svelte';
|
|
35
|
+
export { default as LoadingSection } from './components/LoadingSection.svelte';
|
|
36
|
+
export { default as LiveRegion } from './components/LiveRegion.svelte';
|
|
37
|
+
export { default as SkipLink } from './components/SkipLink.svelte';
|
|
32
38
|
export { default as Dialog } from './components/Dialog.svelte';
|
|
33
39
|
export { default as AlertDialog } from './components/AlertDialog.svelte';
|
|
34
40
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
@@ -85,6 +91,6 @@ export { generateId, computeDescribedBy } from './utils/form.js';
|
|
|
85
91
|
export { tv, type VariantProps } from 'tailwind-variants';
|
|
86
92
|
export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange, validateProps, createValidator, type ValidationResult, } from './validation.js';
|
|
87
93
|
export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, type PerformanceMark, } from './performance.js';
|
|
88
|
-
export { useForm, useAsync, runAsync, usePersistedForm, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, } from './composables/index.js';
|
|
94
|
+
export { useForm, useAsync, runAsync, usePersistedForm, useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useEscapeKey, useClickOutside, clickOutsideAction, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, type UseFocusTrapOptions, type UseFocusRefOptions, type UseArrowNavigationOptions, type ArrowNavigationOrientation, type UseRovingTabindexOptions, type UseAnnounceOptions, type UseAnnounceReturn, type UseEscapeKeyOptions, type UseClickOutsideOptions, } from './composables/index.js';
|
|
89
95
|
export * from './schemas/index.js';
|
|
90
96
|
export { type Notice, type NoticeConfig, type NoticeLocation, type NoticeVariant, type NoticePage, type ActiveNotice, NoticeSchema, NoticeLinkSchema, validateNotice, validateNotices, initializeNotices, setCurrentPath, dismissNotice, clearDismissedNotices, isNoticeDismissed, activeNotices, bannerNotices, cardNotices, getNoticesForPage, refreshNotices, cleanupDismissedNotices, isNoticeActive, isNoticeForPage, isNoticeForLocation, sanitizeHtml, initSanitizer, NoticeBanner, NoticeCard, } from './notices/index.js';
|
package/dist/lib/index.js
CHANGED
|
@@ -34,6 +34,14 @@ export { default as Spinner } from './components/Spinner.svelte';
|
|
|
34
34
|
export { default as Skeleton } from './components/Skeleton.svelte';
|
|
35
35
|
export { default as Toast } from './components/Toast.svelte';
|
|
36
36
|
export { default as ToastContainer } from './components/ToastContainer.svelte';
|
|
37
|
+
// Loading Components
|
|
38
|
+
export { default as ProgressBar } from './components/ProgressBar.svelte';
|
|
39
|
+
export { default as LoadingOverlay } from './components/LoadingOverlay.svelte';
|
|
40
|
+
export { default as LoadingPage } from './components/LoadingPage.svelte';
|
|
41
|
+
export { default as LoadingSection } from './components/LoadingSection.svelte';
|
|
42
|
+
// Accessibility Components
|
|
43
|
+
export { default as LiveRegion } from './components/LiveRegion.svelte';
|
|
44
|
+
export { default as SkipLink } from './components/SkipLink.svelte';
|
|
37
45
|
// Overlay Components (using Bits UI)
|
|
38
46
|
export { default as Dialog } from './components/Dialog.svelte';
|
|
39
47
|
export { default as AlertDialog } from './components/AlertDialog.svelte';
|
|
@@ -112,7 +120,9 @@ export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange,
|
|
|
112
120
|
// Performance monitoring utilities (dev-only, tree-shaken in production)
|
|
113
121
|
export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, } from './performance.js';
|
|
114
122
|
// Composables - reusable Svelte 5 state management patterns
|
|
115
|
-
export { useForm, useAsync, runAsync, usePersistedForm,
|
|
123
|
+
export { useForm, useAsync, runAsync, usePersistedForm,
|
|
124
|
+
// Accessibility composables
|
|
125
|
+
useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useEscapeKey, useClickOutside, clickOutsideAction, } from './composables/index.js';
|
|
116
126
|
// Validation schemas - Zod schemas for forms and data validation
|
|
117
127
|
export * from './schemas/index.js';
|
|
118
128
|
// Notice System - Scheduled, page-specific, dismissible announcements
|
|
@@ -30,7 +30,7 @@ declare class ThemeStore {
|
|
|
30
30
|
/** Current theme mode setting (light, dark, or system) */
|
|
31
31
|
mode: ThemeMode;
|
|
32
32
|
/** Resolved theme (accounts for system preference when mode is 'system') */
|
|
33
|
-
resolvedTheme: "
|
|
33
|
+
resolvedTheme: "light" | "dark";
|
|
34
34
|
/** Whether we're on the client (not SSR) */
|
|
35
35
|
private isClient;
|
|
36
36
|
/** Media query for system dark mode preference */
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ import { type ClassValue } from 'clsx';
|
|
|
6
6
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
7
7
|
/**
|
|
8
8
|
* Generate a unique ID with an optional prefix.
|
|
9
|
-
*
|
|
9
|
+
* Uses crypto.randomUUID() to avoid SSR hydration mismatches caused by
|
|
10
|
+
* sequential counters (where server and client counts can diverge).
|
|
11
|
+
* The short suffix is derived from the UUID for compact, readable IDs.
|
|
10
12
|
*
|
|
11
13
|
* @param prefix - Optional prefix for the ID (e.g., 'checkbox', 'tabs')
|
|
12
|
-
* @returns A unique ID string like 'checkbox-
|
|
14
|
+
* @returns A unique ID string like 'checkbox-a1b2c3d4' or 'ct-a1b2c3d4'
|
|
13
15
|
*/
|
|
14
16
|
export declare function useId(prefix?: string): string;
|
package/dist/lib/utils.js
CHANGED
|
@@ -7,22 +7,16 @@ import { twMerge } from 'tailwind-merge';
|
|
|
7
7
|
export function cn(...inputs) {
|
|
8
8
|
return twMerge(clsx(inputs));
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Counter for generating unique sequential IDs.
|
|
12
|
-
* Uses a sequential counter instead of Math.random() to ensure:
|
|
13
|
-
* - Deterministic IDs that match between SSR and hydration
|
|
14
|
-
* - Stable IDs across component re-renders
|
|
15
|
-
* - Accessibility-friendly ID associations
|
|
16
|
-
*/
|
|
17
|
-
let idCounter = 0;
|
|
18
10
|
/**
|
|
19
11
|
* Generate a unique ID with an optional prefix.
|
|
20
|
-
*
|
|
12
|
+
* Uses crypto.randomUUID() to avoid SSR hydration mismatches caused by
|
|
13
|
+
* sequential counters (where server and client counts can diverge).
|
|
14
|
+
* The short suffix is derived from the UUID for compact, readable IDs.
|
|
21
15
|
*
|
|
22
16
|
* @param prefix - Optional prefix for the ID (e.g., 'checkbox', 'tabs')
|
|
23
|
-
* @returns A unique ID string like 'checkbox-
|
|
17
|
+
* @returns A unique ID string like 'checkbox-a1b2c3d4' or 'ct-a1b2c3d4'
|
|
24
18
|
*/
|
|
25
19
|
export function useId(prefix) {
|
|
26
|
-
const id =
|
|
20
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
27
21
|
return prefix ? `${prefix}-${id}` : `ct-${id}`;
|
|
28
22
|
}
|