sv5ui 1.4.0 → 1.5.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.
- package/README.md +60 -183
- package/dist/Checkbox/Checkbox.svelte +2 -11
- package/dist/CheckboxGroup/CheckboxGroup.svelte +2 -11
- package/dist/FormField/FormField.svelte +2 -6
- package/dist/Input/Input.svelte +2 -10
- package/dist/PinInput/PinInput.svelte +2 -11
- package/dist/RadioGroup/RadioGroup.svelte +2 -11
- package/dist/Select/Select.svelte +2 -10
- package/dist/SelectMenu/SelectMenu.svelte +2 -10
- package/dist/Slider/Slider.svelte +2 -11
- package/dist/Switch/Switch.svelte +2 -11
- package/dist/Table/Table.svelte +763 -0
- package/dist/Table/Table.svelte.d.ts +26 -0
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.js +1 -0
- package/dist/Table/table.types.d.ts +202 -0
- package/dist/Table/table.types.js +1 -0
- package/dist/Table/table.utils.d.ts +51 -0
- package/dist/Table/table.utils.js +166 -0
- package/dist/Table/table.variants.d.ts +205 -0
- package/dist/Table/table.variants.js +126 -0
- package/dist/Textarea/Textarea.svelte +2 -10
- package/dist/config.d.ts +3 -0
- package/dist/config.js +4 -1
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/useClickOutside.svelte.d.ts +31 -0
- package/dist/hooks/useClickOutside.svelte.js +37 -0
- package/dist/hooks/useClipboard.svelte.d.ts +30 -0
- package/dist/hooks/useClipboard.svelte.js +45 -0
- package/dist/hooks/useDebounce.svelte.d.ts +36 -0
- package/dist/hooks/useDebounce.svelte.js +56 -0
- package/dist/hooks/useEscapeKeydown.svelte.d.ts +31 -0
- package/dist/hooks/useEscapeKeydown.svelte.js +37 -0
- package/dist/hooks/useFormField.svelte.d.ts +21 -0
- package/dist/hooks/useFormField.svelte.js +17 -0
- package/dist/hooks/useInfiniteScroll.svelte.d.ts +57 -0
- package/dist/hooks/useInfiniteScroll.svelte.js +69 -0
- package/dist/hooks/useMediaQuery.svelte.d.ts +31 -0
- package/dist/hooks/useMediaQuery.svelte.js +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
export const tableVariants = tv({
|
|
3
|
+
slots: {
|
|
4
|
+
root: 'relative w-full overflow-x-auto scrollbar-thin rounded-xl border border-outline-variant/50 bg-surface [contain:inline-size]',
|
|
5
|
+
base: 'min-w-full',
|
|
6
|
+
caption: 'sr-only',
|
|
7
|
+
thead: 'relative bg-surface-container-low',
|
|
8
|
+
tbody: [
|
|
9
|
+
'[&>tr]:transition-colors [&>tr]:duration-150',
|
|
10
|
+
'[&>tr]:border-b [&>tr]:border-outline-variant/30',
|
|
11
|
+
'[&>tr:last-child]:border-b-0'
|
|
12
|
+
],
|
|
13
|
+
tfoot: 'relative bg-surface-container-low border-t border-outline-variant/50',
|
|
14
|
+
tr: 'data-[selected=true]:bg-primary-container/20 data-[pinned-row=true]:bg-surface-container-lowest',
|
|
15
|
+
th: [
|
|
16
|
+
'relative px-4 py-3 text-xs font-semibold uppercase tracking-wider',
|
|
17
|
+
'text-on-surface-variant text-left rtl:text-right',
|
|
18
|
+
'[&:has([role=checkbox])]:pe-0 [&:has([role=checkbox])]:w-12'
|
|
19
|
+
],
|
|
20
|
+
td: [
|
|
21
|
+
'px-4 py-3.5 text-sm text-on-surface',
|
|
22
|
+
'whitespace-nowrap max-sm:whitespace-normal max-sm:break-words',
|
|
23
|
+
'[&:has([role=checkbox])]:pe-0 [&:has([role=checkbox])]:w-12'
|
|
24
|
+
],
|
|
25
|
+
separator: 'h-px bg-outline-variant/50',
|
|
26
|
+
empty: 'py-10 text-center text-sm text-on-surface-variant/70',
|
|
27
|
+
loading: 'py-10 text-center'
|
|
28
|
+
},
|
|
29
|
+
variants: {
|
|
30
|
+
pinned: {
|
|
31
|
+
true: {
|
|
32
|
+
th: 'sticky bg-surface-container-low/95 backdrop-blur-sm z-1',
|
|
33
|
+
td: 'sticky bg-surface/95 backdrop-blur-sm z-1'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
hoverable: {
|
|
37
|
+
true: {
|
|
38
|
+
tbody: '[&>tr]:data-[selectable=true]:cursor-pointer [&>tr]:data-[selectable=true]:hover:bg-surface-container/60 [&>tr]:data-[selectable=true]:focus-visible:outline-2 [&>tr]:data-[selectable=true]:focus-visible:outline-primary [&>tr]:data-[selectable=true]:focus-visible:outline-offset-[-2px]'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
sticky: {
|
|
42
|
+
true: {
|
|
43
|
+
thead: 'sticky top-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-t-xl',
|
|
44
|
+
tfoot: 'sticky bottom-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-b-xl'
|
|
45
|
+
},
|
|
46
|
+
header: {
|
|
47
|
+
thead: 'sticky top-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-t-xl'
|
|
48
|
+
},
|
|
49
|
+
footer: {
|
|
50
|
+
tfoot: 'sticky bottom-0 inset-x-0 bg-surface-container-low/95 backdrop-blur-sm z-10 rounded-b-xl'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
striped: {
|
|
54
|
+
true: {
|
|
55
|
+
tbody: '[&>tr:nth-child(even)]:bg-surface-container-lowest/60'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
loading: {
|
|
59
|
+
true: {
|
|
60
|
+
thead: 'after:absolute after:top-0 after:left-0 after:z-20 after:h-0.5 after:rounded-full'
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
loadingColor: {
|
|
64
|
+
primary: '',
|
|
65
|
+
secondary: '',
|
|
66
|
+
tertiary: '',
|
|
67
|
+
success: '',
|
|
68
|
+
warning: '',
|
|
69
|
+
error: '',
|
|
70
|
+
info: '',
|
|
71
|
+
surface: ''
|
|
72
|
+
},
|
|
73
|
+
loadingAnimation: {
|
|
74
|
+
carousel: '',
|
|
75
|
+
'carousel-inverse': '',
|
|
76
|
+
swing: '',
|
|
77
|
+
elastic: ''
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
compoundVariants: [
|
|
81
|
+
// ========== LOADING COLOR ==========
|
|
82
|
+
{ loading: true, loadingColor: 'primary', class: { thead: 'after:bg-primary' } },
|
|
83
|
+
{ loading: true, loadingColor: 'secondary', class: { thead: 'after:bg-secondary' } },
|
|
84
|
+
{ loading: true, loadingColor: 'tertiary', class: { thead: 'after:bg-tertiary' } },
|
|
85
|
+
{ loading: true, loadingColor: 'success', class: { thead: 'after:bg-success' } },
|
|
86
|
+
{ loading: true, loadingColor: 'warning', class: { thead: 'after:bg-warning' } },
|
|
87
|
+
{ loading: true, loadingColor: 'error', class: { thead: 'after:bg-error' } },
|
|
88
|
+
{ loading: true, loadingColor: 'info', class: { thead: 'after:bg-info' } },
|
|
89
|
+
{ loading: true, loadingColor: 'surface', class: { thead: 'after:bg-on-surface' } },
|
|
90
|
+
// ========== LOADING ANIMATION ==========
|
|
91
|
+
{
|
|
92
|
+
loading: true,
|
|
93
|
+
loadingAnimation: 'carousel',
|
|
94
|
+
class: { thead: 'after:animate-[carousel_2s_ease-in-out_infinite]' }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
loading: true,
|
|
98
|
+
loadingAnimation: 'carousel-inverse',
|
|
99
|
+
class: { thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite]' }
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
loading: true,
|
|
103
|
+
loadingAnimation: 'swing',
|
|
104
|
+
class: { thead: 'after:animate-[swing_2s_ease-in-out_infinite]' }
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
loading: true,
|
|
108
|
+
loadingAnimation: 'elastic',
|
|
109
|
+
class: { thead: 'after:animate-[elastic_2s_ease-in-out_infinite]' }
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
defaultVariants: {
|
|
113
|
+
hoverable: true,
|
|
114
|
+
loadingColor: 'primary',
|
|
115
|
+
loadingAnimation: 'carousel'
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
export const tableDefaults = {
|
|
119
|
+
defaultVariants: {
|
|
120
|
+
...tableVariants.defaultVariants,
|
|
121
|
+
hoverable: true,
|
|
122
|
+
loadingColor: 'primary',
|
|
123
|
+
loadingAnimation: 'carousel'
|
|
124
|
+
},
|
|
125
|
+
slots: {}
|
|
126
|
+
};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
type FieldGroupVariantProps
|
|
14
14
|
} from '../FieldGroup/field-group.variants.js'
|
|
15
15
|
import Icon from '../Icon/Icon.svelte'
|
|
16
|
-
import
|
|
16
|
+
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
17
17
|
|
|
18
18
|
const config = getComponentConfig('textarea', textareaDefaults)
|
|
19
19
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -44,15 +44,7 @@
|
|
|
44
44
|
...restProps
|
|
45
45
|
}: Props = $props()
|
|
46
46
|
|
|
47
|
-
const formFieldContext =
|
|
48
|
-
| {
|
|
49
|
-
name?: string
|
|
50
|
-
size: NonNullable<FormFieldProps['size']>
|
|
51
|
-
error?: string | boolean
|
|
52
|
-
ariaId: string
|
|
53
|
-
}
|
|
54
|
-
| undefined
|
|
55
|
-
>('formField')
|
|
47
|
+
const formFieldContext = useFormField()
|
|
56
48
|
|
|
57
49
|
const fieldGroupContext = getContext<
|
|
58
50
|
| {
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -38,7 +38,10 @@ export const iconsDefaults = {
|
|
|
38
38
|
file: 'lucide:file',
|
|
39
39
|
zoomIn: 'lucide:zoom-in',
|
|
40
40
|
trash: 'lucide:trash-2',
|
|
41
|
-
search: 'lucide:search'
|
|
41
|
+
search: 'lucide:search',
|
|
42
|
+
sortAsc: 'lucide:chevron-up',
|
|
43
|
+
sortDesc: 'lucide:chevron-down',
|
|
44
|
+
sortDefault: 'lucide:chevrons-up-down'
|
|
42
45
|
};
|
|
43
46
|
// ==================== GLOBAL STATE ====================
|
|
44
47
|
let globalConfig = {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { useMediaQuery } from './useMediaQuery.svelte.js';
|
|
2
|
+
export type { UseMediaQueryOptions } from './useMediaQuery.svelte.js';
|
|
3
|
+
export { useClipboard } from './useClipboard.svelte.js';
|
|
4
|
+
export type { UseClipboardOptions } from './useClipboard.svelte.js';
|
|
5
|
+
export { useFormField } from './useFormField.svelte.js';
|
|
6
|
+
export type { FormFieldContext } from './useFormField.svelte.js';
|
|
7
|
+
export { useClickOutside } from './useClickOutside.svelte.js';
|
|
8
|
+
export type { UseClickOutsideOptions } from './useClickOutside.svelte.js';
|
|
9
|
+
export { useInfiniteScroll } from './useInfiniteScroll.svelte.js';
|
|
10
|
+
export type { UseInfiniteScrollOptions } from './useInfiniteScroll.svelte.js';
|
|
11
|
+
export { useEscapeKeydown } from './useEscapeKeydown.svelte.js';
|
|
12
|
+
export type { UseEscapeKeydownOptions } from './useEscapeKeydown.svelte.js';
|
|
13
|
+
export { useDebounce } from './useDebounce.svelte.js';
|
|
14
|
+
export type { UseDebounceOptions } from './useDebounce.svelte.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useMediaQuery } from './useMediaQuery.svelte.js';
|
|
2
|
+
export { useClipboard } from './useClipboard.svelte.js';
|
|
3
|
+
export { useFormField } from './useFormField.svelte.js';
|
|
4
|
+
export { useClickOutside } from './useClickOutside.svelte.js';
|
|
5
|
+
export { useInfiniteScroll } from './useInfiniteScroll.svelte.js';
|
|
6
|
+
export { useEscapeKeydown } from './useEscapeKeydown.svelte.js';
|
|
7
|
+
export { useDebounce } from './useDebounce.svelte.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action';
|
|
2
|
+
export interface UseClickOutsideOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Callback fired when a click occurs outside the element.
|
|
5
|
+
*/
|
|
6
|
+
handler: (event: PointerEvent) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the listener is active.
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Svelte action that detects clicks outside an element.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <script>
|
|
19
|
+
* import { useClickOutside } from 'sv5ui'
|
|
20
|
+
*
|
|
21
|
+
* let open = $state(false)
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* {#if open}
|
|
25
|
+
* <div use:useClickOutside={{ handler: () => (open = false) }}>
|
|
26
|
+
* Dropdown content
|
|
27
|
+
* </div>
|
|
28
|
+
* {/if}
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const useClickOutside: Action<HTMLElement, UseClickOutsideOptions>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action that detects clicks outside an element.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <script>
|
|
7
|
+
* import { useClickOutside } from 'sv5ui'
|
|
8
|
+
*
|
|
9
|
+
* let open = $state(false)
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* {#if open}
|
|
13
|
+
* <div use:useClickOutside={{ handler: () => (open = false) }}>
|
|
14
|
+
* Dropdown content
|
|
15
|
+
* </div>
|
|
16
|
+
* {/if}
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const useClickOutside = (node, initialOptions) => {
|
|
20
|
+
let currentOptions = initialOptions;
|
|
21
|
+
function handlePointerDown(event) {
|
|
22
|
+
if (currentOptions.enabled === false)
|
|
23
|
+
return;
|
|
24
|
+
if (!node.contains(event.target)) {
|
|
25
|
+
currentOptions.handler(event);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
document.addEventListener('pointerdown', handlePointerDown, true);
|
|
29
|
+
return {
|
|
30
|
+
update(newOptions) {
|
|
31
|
+
currentOptions = newOptions;
|
|
32
|
+
},
|
|
33
|
+
destroy() {
|
|
34
|
+
document.removeEventListener('pointerdown', handlePointerDown, true);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface UseClipboardOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Duration in milliseconds before `copied` resets to `false`.
|
|
4
|
+
* @default 2000
|
|
5
|
+
*/
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface UseClipboardReturn {
|
|
9
|
+
/** Whether text was recently copied (resets after timeout) */
|
|
10
|
+
readonly copied: boolean;
|
|
11
|
+
/** Copy text to clipboard */
|
|
12
|
+
copy: (text: string) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reactive clipboard hook. Copies text and tracks copied state.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <script>
|
|
20
|
+
* import { useClipboard } from 'sv5ui'
|
|
21
|
+
*
|
|
22
|
+
* const clipboard = useClipboard()
|
|
23
|
+
* </script>
|
|
24
|
+
*
|
|
25
|
+
* <Button onclick={() => clipboard.copy('Hello!')}>
|
|
26
|
+
* {clipboard.copied ? 'Copied!' : 'Copy'}
|
|
27
|
+
* </Button>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive clipboard hook. Copies text and tracks copied state.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <script>
|
|
7
|
+
* import { useClipboard } from 'sv5ui'
|
|
8
|
+
*
|
|
9
|
+
* const clipboard = useClipboard()
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* <Button onclick={() => clipboard.copy('Hello!')}>
|
|
13
|
+
* {clipboard.copied ? 'Copied!' : 'Copy'}
|
|
14
|
+
* </Button>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function useClipboard(options = {}) {
|
|
18
|
+
const { timeout = 2000 } = options;
|
|
19
|
+
let copied = $state(false);
|
|
20
|
+
let timeoutId;
|
|
21
|
+
async function copy(text) {
|
|
22
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
await navigator.clipboard.writeText(text);
|
|
26
|
+
copied = true;
|
|
27
|
+
clearTimeout(timeoutId);
|
|
28
|
+
timeoutId = setTimeout(() => {
|
|
29
|
+
copied = false;
|
|
30
|
+
}, timeout);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
copied = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
$effect(() => {
|
|
37
|
+
return () => clearTimeout(timeoutId);
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
get copied() {
|
|
41
|
+
return copied;
|
|
42
|
+
},
|
|
43
|
+
copy
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface UseDebounceOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Delay in milliseconds before the callback is executed.
|
|
4
|
+
* @default 300
|
|
5
|
+
*/
|
|
6
|
+
delay?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reactive debounce hook. Delays execution until a pause in calls.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script>
|
|
14
|
+
* import { useDebounce } from 'sv5ui'
|
|
15
|
+
*
|
|
16
|
+
* const debounce = useDebounce({ delay: 500 })
|
|
17
|
+
* let query = $state('')
|
|
18
|
+
*
|
|
19
|
+
* function handleInput(e: Event) {
|
|
20
|
+
* query = e.currentTarget.value
|
|
21
|
+
* debounce.run(() => fetchResults(query))
|
|
22
|
+
* }
|
|
23
|
+
* </script>
|
|
24
|
+
*
|
|
25
|
+
* <input oninput={handleInput} />
|
|
26
|
+
* {#if debounce.pending}
|
|
27
|
+
* <Spinner />
|
|
28
|
+
* {/if}
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function useDebounce(options?: UseDebounceOptions): {
|
|
32
|
+
readonly pending: boolean;
|
|
33
|
+
run: (callback: () => void) => void;
|
|
34
|
+
cancel: () => void;
|
|
35
|
+
flush: (callback: () => void) => void;
|
|
36
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive debounce hook. Delays execution until a pause in calls.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <script>
|
|
7
|
+
* import { useDebounce } from 'sv5ui'
|
|
8
|
+
*
|
|
9
|
+
* const debounce = useDebounce({ delay: 500 })
|
|
10
|
+
* let query = $state('')
|
|
11
|
+
*
|
|
12
|
+
* function handleInput(e: Event) {
|
|
13
|
+
* query = e.currentTarget.value
|
|
14
|
+
* debounce.run(() => fetchResults(query))
|
|
15
|
+
* }
|
|
16
|
+
* </script>
|
|
17
|
+
*
|
|
18
|
+
* <input oninput={handleInput} />
|
|
19
|
+
* {#if debounce.pending}
|
|
20
|
+
* <Spinner />
|
|
21
|
+
* {/if}
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useDebounce(options = {}) {
|
|
25
|
+
const { delay = 300 } = options;
|
|
26
|
+
let pending = $state(false);
|
|
27
|
+
let timeoutId;
|
|
28
|
+
function run(callback) {
|
|
29
|
+
pending = true;
|
|
30
|
+
clearTimeout(timeoutId);
|
|
31
|
+
timeoutId = setTimeout(() => {
|
|
32
|
+
pending = false;
|
|
33
|
+
callback();
|
|
34
|
+
}, delay);
|
|
35
|
+
}
|
|
36
|
+
function cancel() {
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
pending = false;
|
|
39
|
+
}
|
|
40
|
+
function flush(callback) {
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
pending = false;
|
|
43
|
+
callback();
|
|
44
|
+
}
|
|
45
|
+
$effect(() => {
|
|
46
|
+
return () => clearTimeout(timeoutId);
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
get pending() {
|
|
50
|
+
return pending;
|
|
51
|
+
},
|
|
52
|
+
run,
|
|
53
|
+
cancel,
|
|
54
|
+
flush
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action';
|
|
2
|
+
export interface UseEscapeKeydownOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Callback fired when Escape is pressed.
|
|
5
|
+
*/
|
|
6
|
+
handler: (event: KeyboardEvent) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the listener is active.
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Svelte action that listens for Escape keydown on an element or the document.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <script>
|
|
19
|
+
* import { useEscapeKeydown } from 'sv5ui'
|
|
20
|
+
*
|
|
21
|
+
* let open = $state(true)
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* {#if open}
|
|
25
|
+
* <div use:useEscapeKeydown={{ handler: () => (open = false) }}>
|
|
26
|
+
* Press Escape to close
|
|
27
|
+
* </div>
|
|
28
|
+
* {/if}
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const useEscapeKeydown: Action<HTMLElement, UseEscapeKeydownOptions>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action that listens for Escape keydown on an element or the document.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <script>
|
|
7
|
+
* import { useEscapeKeydown } from 'sv5ui'
|
|
8
|
+
*
|
|
9
|
+
* let open = $state(true)
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* {#if open}
|
|
13
|
+
* <div use:useEscapeKeydown={{ handler: () => (open = false) }}>
|
|
14
|
+
* Press Escape to close
|
|
15
|
+
* </div>
|
|
16
|
+
* {/if}
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const useEscapeKeydown = (_node, initialOptions) => {
|
|
20
|
+
let currentOptions = initialOptions;
|
|
21
|
+
function handleKeydown(event) {
|
|
22
|
+
if (currentOptions.enabled === false)
|
|
23
|
+
return;
|
|
24
|
+
if (event.key === 'Escape') {
|
|
25
|
+
currentOptions.handler(event);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
document.addEventListener('keydown', handleKeydown);
|
|
29
|
+
return {
|
|
30
|
+
update(newOptions) {
|
|
31
|
+
currentOptions = newOptions;
|
|
32
|
+
},
|
|
33
|
+
destroy() {
|
|
34
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormFieldProps } from '../FormField/form-field.types.js';
|
|
2
|
+
export interface FormFieldContext {
|
|
3
|
+
name?: string;
|
|
4
|
+
size: NonNullable<FormFieldProps['size']>;
|
|
5
|
+
error?: string | boolean;
|
|
6
|
+
ariaId: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Access the nearest FormField context. Returns `undefined` when used outside a FormField.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script>
|
|
14
|
+
* import { useFormField } from 'sv5ui'
|
|
15
|
+
*
|
|
16
|
+
* const formField = useFormField()
|
|
17
|
+
* const hasError = $derived(formField?.error !== undefined && formField?.error !== false)
|
|
18
|
+
* </script>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function useFormField(): FormFieldContext | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getContext } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Access the nearest FormField context. Returns `undefined` when used outside a FormField.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```svelte
|
|
7
|
+
* <script>
|
|
8
|
+
* import { useFormField } from 'sv5ui'
|
|
9
|
+
*
|
|
10
|
+
* const formField = useFormField()
|
|
11
|
+
* const hasError = $derived(formField?.error !== undefined && formField?.error !== false)
|
|
12
|
+
* </script>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useFormField() {
|
|
16
|
+
return getContext('formField');
|
|
17
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action';
|
|
2
|
+
export interface UseInfiniteScrollOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Callback fired when the user scrolls near the bottom.
|
|
5
|
+
* Return a promise to automatically manage the loading state.
|
|
6
|
+
*/
|
|
7
|
+
onLoad: () => void | Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Distance in pixels from the bottom to trigger loading.
|
|
10
|
+
* @default 100
|
|
11
|
+
*/
|
|
12
|
+
threshold?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the hook is active. Set to `false` when all data is loaded.
|
|
15
|
+
* Supports getter function for reactive control.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
enabled?: boolean | (() => boolean);
|
|
19
|
+
}
|
|
20
|
+
export interface UseInfiniteScrollReturn {
|
|
21
|
+
/** Whether an async onLoad is currently in progress */
|
|
22
|
+
readonly loading: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reactive infinite scroll hook with Svelte action.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```svelte
|
|
29
|
+
* <script>
|
|
30
|
+
* import { useInfiniteScroll } from 'sv5ui'
|
|
31
|
+
*
|
|
32
|
+
* let items = $state([...])
|
|
33
|
+
* let hasMore = $state(true)
|
|
34
|
+
*
|
|
35
|
+
* const scroll = useInfiniteScroll({
|
|
36
|
+
* onLoad: async () => {
|
|
37
|
+
* const next = await fetchMore()
|
|
38
|
+
* items.push(...next)
|
|
39
|
+
* if (next.length === 0) hasMore = false
|
|
40
|
+
* },
|
|
41
|
+
* enabled: () => hasMore
|
|
42
|
+
* })
|
|
43
|
+
* </script>
|
|
44
|
+
*
|
|
45
|
+
* <div use:scroll.action>
|
|
46
|
+
* {#each items as item (item.id)}
|
|
47
|
+
* <div>{item.name}</div>
|
|
48
|
+
* {/each}
|
|
49
|
+
* {#if scroll.loading}
|
|
50
|
+
* <Spinner />
|
|
51
|
+
* {/if}
|
|
52
|
+
* </div>
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function useInfiniteScroll(options: UseInfiniteScrollOptions): UseInfiniteScrollReturn & {
|
|
56
|
+
action: Action<HTMLElement>;
|
|
57
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive infinite scroll hook with Svelte action.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <script>
|
|
7
|
+
* import { useInfiniteScroll } from 'sv5ui'
|
|
8
|
+
*
|
|
9
|
+
* let items = $state([...])
|
|
10
|
+
* let hasMore = $state(true)
|
|
11
|
+
*
|
|
12
|
+
* const scroll = useInfiniteScroll({
|
|
13
|
+
* onLoad: async () => {
|
|
14
|
+
* const next = await fetchMore()
|
|
15
|
+
* items.push(...next)
|
|
16
|
+
* if (next.length === 0) hasMore = false
|
|
17
|
+
* },
|
|
18
|
+
* enabled: () => hasMore
|
|
19
|
+
* })
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <div use:scroll.action>
|
|
23
|
+
* {#each items as item (item.id)}
|
|
24
|
+
* <div>{item.name}</div>
|
|
25
|
+
* {/each}
|
|
26
|
+
* {#if scroll.loading}
|
|
27
|
+
* <Spinner />
|
|
28
|
+
* {/if}
|
|
29
|
+
* </div>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useInfiniteScroll(options) {
|
|
33
|
+
const { onLoad, threshold = 100 } = options;
|
|
34
|
+
let loading = $state(false);
|
|
35
|
+
function getEnabled() {
|
|
36
|
+
return typeof options.enabled === 'function' ? options.enabled() : (options.enabled ?? true);
|
|
37
|
+
}
|
|
38
|
+
async function check(el) {
|
|
39
|
+
if (loading || !getEnabled())
|
|
40
|
+
return;
|
|
41
|
+
const { scrollTop, scrollHeight, clientHeight } = el;
|
|
42
|
+
if (scrollHeight - scrollTop - clientHeight > threshold)
|
|
43
|
+
return;
|
|
44
|
+
loading = true;
|
|
45
|
+
try {
|
|
46
|
+
await onLoad();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
loading = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const action = (node) => {
|
|
53
|
+
function handleScroll() {
|
|
54
|
+
check(node);
|
|
55
|
+
}
|
|
56
|
+
node.addEventListener('scroll', handleScroll, { passive: true });
|
|
57
|
+
return {
|
|
58
|
+
destroy() {
|
|
59
|
+
node.removeEventListener('scroll', handleScroll);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
get loading() {
|
|
65
|
+
return loading;
|
|
66
|
+
},
|
|
67
|
+
action
|
|
68
|
+
};
|
|
69
|
+
}
|