sv5ui 1.3.0 → 1.5.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.
- package/README.md +16 -11
- package/dist/Checkbox/Checkbox.svelte +2 -11
- package/dist/CheckboxGroup/CheckboxGroup.svelte +2 -11
- package/dist/Collapsible/Collapsible.svelte +69 -0
- package/dist/Collapsible/Collapsible.svelte.d.ts +6 -0
- package/dist/Collapsible/CollapsibleTestWrapper.svelte +17 -0
- package/dist/Collapsible/CollapsibleTestWrapper.svelte.d.ts +4 -0
- package/dist/Collapsible/collapsible.types.d.ts +75 -0
- package/dist/Collapsible/collapsible.types.js +1 -0
- package/dist/Collapsible/collapsible.variants.d.ts +53 -0
- package/dist/Collapsible/collapsible.variants.js +21 -0
- package/dist/Collapsible/index.d.ts +2 -0
- package/dist/Collapsible/index.js +1 -0
- package/dist/Command/Command.svelte +183 -0
- package/dist/Command/Command.svelte.d.ts +6 -0
- package/dist/Command/CommandTestWrapper.svelte +13 -0
- package/dist/Command/CommandTestWrapper.svelte.d.ts +4 -0
- package/dist/Command/command.types.d.ts +98 -0
- package/dist/Command/command.types.js +1 -0
- package/dist/Command/command.variants.d.ts +226 -0
- package/dist/Command/command.variants.js +86 -0
- package/dist/Command/index.d.ts +2 -0
- package/dist/Command/index.js +1 -0
- 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/Select/select.variants.js +1 -1
- package/dist/SelectMenu/SelectMenu.svelte +2 -10
- package/dist/SelectMenu/select-menu.variants.js +1 -1
- package/dist/Slider/Slider.svelte +2 -11
- package/dist/Switch/Switch.svelte +2 -11
- package/dist/Table/Table.svelte +752 -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 +199 -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/Toast/Toaster.svelte +618 -0
- package/dist/Toast/Toaster.svelte.d.ts +5 -0
- package/dist/Toast/index.d.ts +4 -0
- package/dist/Toast/index.js +2 -0
- package/dist/Toast/toast.d.ts +38 -0
- package/dist/Toast/toast.js +73 -0
- package/dist/Toast/toast.types.d.ts +19 -0
- package/dist/Toast/toast.types.js +1 -0
- package/dist/Toast/toast.variants.d.ts +7 -0
- package/dist/Toast/toast.variants.js +5 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +5 -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 +5 -0
- package/dist/index.js +6 -0
- package/dist/theme.css +36 -0
- package/package.json +2 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { toast as sonnerToast } from 'svelte-sonner';
|
|
2
|
+
import { mount, unmount } from 'svelte';
|
|
3
|
+
import Icon from '../Icon/Icon.svelte';
|
|
4
|
+
import Avatar from '../Avatar/Avatar.svelte';
|
|
5
|
+
function createIconComponent(name) {
|
|
6
|
+
function SvelteSonnerIcon(anchor) {
|
|
7
|
+
if (!anchor.parentNode)
|
|
8
|
+
return { destroy() { } };
|
|
9
|
+
const instance = mount(Icon, {
|
|
10
|
+
target: anchor.parentNode,
|
|
11
|
+
anchor,
|
|
12
|
+
props: { name, size: '18' }
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
destroy() {
|
|
16
|
+
unmount(instance);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return SvelteSonnerIcon;
|
|
21
|
+
}
|
|
22
|
+
function createAvatarComponent(props) {
|
|
23
|
+
function SvelteSonnerAvatar(anchor) {
|
|
24
|
+
if (!anchor.parentNode)
|
|
25
|
+
return { destroy() { } };
|
|
26
|
+
const instance = mount(Avatar, {
|
|
27
|
+
target: anchor.parentNode,
|
|
28
|
+
anchor,
|
|
29
|
+
props: { size: 'sm', ...props }
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
destroy() {
|
|
33
|
+
unmount(instance);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return SvelteSonnerAvatar;
|
|
38
|
+
}
|
|
39
|
+
function resolveOptions(data) {
|
|
40
|
+
if (!data)
|
|
41
|
+
return data;
|
|
42
|
+
const { color, icon, avatar, ...rest } = data;
|
|
43
|
+
const resolved = { ...rest };
|
|
44
|
+
// Color -> class
|
|
45
|
+
if (color) {
|
|
46
|
+
const existing = resolved.class ?? '';
|
|
47
|
+
resolved.class = `${existing} sv5ui-color-${color}`.trim();
|
|
48
|
+
}
|
|
49
|
+
// Avatar takes priority over icon
|
|
50
|
+
if (avatar) {
|
|
51
|
+
resolved.icon = createAvatarComponent(avatar);
|
|
52
|
+
}
|
|
53
|
+
else if (typeof icon === 'string') {
|
|
54
|
+
resolved.icon = createIconComponent(icon);
|
|
55
|
+
}
|
|
56
|
+
else if (icon !== undefined) {
|
|
57
|
+
resolved.icon = icon;
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
function toastFn(message, data) {
|
|
62
|
+
return sonnerToast(message, resolveOptions(data));
|
|
63
|
+
}
|
|
64
|
+
toastFn.success = (message, data) => sonnerToast.success(message, resolveOptions(data));
|
|
65
|
+
toastFn.error = (message, data) => sonnerToast.error(message, resolveOptions(data));
|
|
66
|
+
toastFn.warning = (message, data) => sonnerToast.warning(message, resolveOptions(data));
|
|
67
|
+
toastFn.info = (message, data) => sonnerToast.info(message, resolveOptions(data));
|
|
68
|
+
toastFn.loading = (message, data) => sonnerToast.loading(message, resolveOptions(data));
|
|
69
|
+
toastFn.promise = sonnerToast.promise;
|
|
70
|
+
toastFn.dismiss = sonnerToast.dismiss;
|
|
71
|
+
toastFn.custom = sonnerToast.custom;
|
|
72
|
+
toastFn.message = sonnerToast.message;
|
|
73
|
+
export { toastFn as toast };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClassNameValue } from 'tailwind-merge';
|
|
2
|
+
import type { ToasterProps as SonnerToasterProps } from 'svelte-sonner';
|
|
3
|
+
import type { ToastVariant } from './toast.variants.js';
|
|
4
|
+
export type ToasterProps = Omit<SonnerToasterProps, 'class' | 'toastOptions' | 'richColors'> & {
|
|
5
|
+
/**
|
|
6
|
+
* The visual style variant.
|
|
7
|
+
* - `outline`: Border with surface background, semantic border accent per type (default)
|
|
8
|
+
* - `soft`: Light tinted background per type
|
|
9
|
+
* - `subtle`: Light tinted background + semantic border per type
|
|
10
|
+
* - `solid`: Full semantic color background per type
|
|
11
|
+
* - `accent`: Left color stripe with surface background
|
|
12
|
+
* @default 'outline'
|
|
13
|
+
*/
|
|
14
|
+
variant?: ToastVariant;
|
|
15
|
+
/**
|
|
16
|
+
* Additional CSS classes for the toaster container.
|
|
17
|
+
*/
|
|
18
|
+
class?: ClassNameValue;
|
|
19
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ToastVariant = 'solid' | 'outline' | 'soft' | 'subtle' | 'accent';
|
|
2
|
+
export type ToastColor = 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'error' | 'info';
|
|
3
|
+
export declare const toastDefaults: {
|
|
4
|
+
defaultVariants: {
|
|
5
|
+
variant: ToastVariant;
|
|
6
|
+
};
|
|
7
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ export declare const iconsDefaults: {
|
|
|
37
37
|
file: string;
|
|
38
38
|
zoomIn: string;
|
|
39
39
|
trash: string;
|
|
40
|
+
search: string;
|
|
41
|
+
sortAsc: string;
|
|
42
|
+
sortDesc: string;
|
|
43
|
+
sortDefault: string;
|
|
40
44
|
};
|
|
41
45
|
/** Deep partial type for config objects */
|
|
42
46
|
type DeepPartial<T> = {
|
package/dist/config.js
CHANGED
|
@@ -37,7 +37,11 @@ export const iconsDefaults = {
|
|
|
37
37
|
upload: 'lucide:upload',
|
|
38
38
|
file: 'lucide:file',
|
|
39
39
|
zoomIn: 'lucide:zoom-in',
|
|
40
|
-
trash: 'lucide:trash-2'
|
|
40
|
+
trash: 'lucide:trash-2',
|
|
41
|
+
search: 'lucide:search',
|
|
42
|
+
sortAsc: 'lucide:chevron-up',
|
|
43
|
+
sortDesc: 'lucide:chevron-down',
|
|
44
|
+
sortDefault: 'lucide:chevrons-up-down'
|
|
41
45
|
};
|
|
42
46
|
// ==================== GLOBAL STATE ====================
|
|
43
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface UseMediaQueryOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Initial value before the media query is evaluated (SSR-safe).
|
|
4
|
+
* @default false
|
|
5
|
+
*/
|
|
6
|
+
initialValue?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface UseMediaQueryReturn {
|
|
9
|
+
/** Whether the media query currently matches */
|
|
10
|
+
readonly matches: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Reactive media query hook. Tracks whether a CSS media query matches.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { useMediaQuery } from 'sv5ui'
|
|
19
|
+
*
|
|
20
|
+
* const isMobile = useMediaQuery('(max-width: 640px)')
|
|
21
|
+
* const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* {#if isMobile.matches}
|
|
25
|
+
* <MobileLayout />
|
|
26
|
+
* {:else}
|
|
27
|
+
* <DesktopLayout />
|
|
28
|
+
* {/if}
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function useMediaQuery(query: string | (() => string), options?: UseMediaQueryOptions): UseMediaQueryReturn;
|