@lyeve-labs/ui-kit 0.8.2
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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/components/Accordion.svelte +48 -0
- package/dist/components/Accordion.svelte.d.ts +10 -0
- package/dist/components/AccordionItem.svelte +55 -0
- package/dist/components/AccordionItem.svelte.d.ts +10 -0
- package/dist/components/Alert.svelte +60 -0
- package/dist/components/Alert.svelte.d.ts +13 -0
- package/dist/components/Autocomplete.svelte +198 -0
- package/dist/components/Autocomplete.svelte.d.ts +23 -0
- package/dist/components/Avatar.svelte +65 -0
- package/dist/components/Avatar.svelte.d.ts +9 -0
- package/dist/components/AvatarGroup.svelte +45 -0
- package/dist/components/AvatarGroup.svelte.d.ts +14 -0
- package/dist/components/Badge.svelte +48 -0
- package/dist/components/Badge.svelte.d.ts +13 -0
- package/dist/components/Banner.svelte +73 -0
- package/dist/components/Banner.svelte.d.ts +13 -0
- package/dist/components/Breadcrumb.svelte +47 -0
- package/dist/components/Breadcrumb.svelte.d.ts +10 -0
- package/dist/components/Button.svelte +71 -0
- package/dist/components/Button.svelte.d.ts +18 -0
- package/dist/components/ButtonGroup.svelte +33 -0
- package/dist/components/ButtonGroup.svelte.d.ts +9 -0
- package/dist/components/Card.svelte +72 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/Checkbox.svelte +80 -0
- package/dist/components/Checkbox.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +274 -0
- package/dist/components/DatePicker.svelte.d.ts +17 -0
- package/dist/components/Divider.svelte +23 -0
- package/dist/components/Divider.svelte.d.ts +8 -0
- package/dist/components/Drawer.svelte +120 -0
- package/dist/components/Drawer.svelte.d.ts +16 -0
- package/dist/components/Dropdown.svelte +88 -0
- package/dist/components/Dropdown.svelte.d.ts +23 -0
- package/dist/components/EmptyState.svelte +51 -0
- package/dist/components/EmptyState.svelte.d.ts +13 -0
- package/dist/components/FileInput.svelte +102 -0
- package/dist/components/FileInput.svelte.d.ts +15 -0
- package/dist/components/Indicator.svelte +38 -0
- package/dist/components/Indicator.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +60 -0
- package/dist/components/Input.svelte.d.ts +17 -0
- package/dist/components/Kbd.svelte +18 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Label.svelte +30 -0
- package/dist/components/Label.svelte.d.ts +11 -0
- package/dist/components/Modal.svelte +105 -0
- package/dist/components/Modal.svelte.d.ts +14 -0
- package/dist/components/MultiSelect.svelte +212 -0
- package/dist/components/MultiSelect.svelte.d.ts +22 -0
- package/dist/components/NumberInput.svelte +98 -0
- package/dist/components/NumberInput.svelte.d.ts +15 -0
- package/dist/components/PageHeader.svelte +22 -0
- package/dist/components/PageHeader.svelte.d.ts +10 -0
- package/dist/components/Pagination.svelte +112 -0
- package/dist/components/Pagination.svelte.d.ts +10 -0
- package/dist/components/Progress.svelte +70 -0
- package/dist/components/Progress.svelte.d.ts +15 -0
- package/dist/components/Radio.svelte +65 -0
- package/dist/components/Radio.svelte.d.ts +14 -0
- package/dist/components/RadioGroup.svelte +96 -0
- package/dist/components/RadioGroup.svelte.d.ts +22 -0
- package/dist/components/SearchInput.svelte +81 -0
- package/dist/components/SearchInput.svelte.d.ts +10 -0
- package/dist/components/Select.svelte +69 -0
- package/dist/components/Select.svelte.d.ts +18 -0
- package/dist/components/Skeleton.svelte +20 -0
- package/dist/components/Skeleton.svelte.d.ts +9 -0
- package/dist/components/Spinner.svelte +19 -0
- package/dist/components/Spinner.svelte.d.ts +7 -0
- package/dist/components/Stat.svelte +45 -0
- package/dist/components/Stat.svelte.d.ts +14 -0
- package/dist/components/StepIndicator.svelte +142 -0
- package/dist/components/StepIndicator.svelte.d.ts +13 -0
- package/dist/components/Table.svelte +36 -0
- package/dist/components/Table.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +39 -0
- package/dist/components/Tabs.svelte.d.ts +13 -0
- package/dist/components/Tag.svelte +56 -0
- package/dist/components/Tag.svelte.d.ts +11 -0
- package/dist/components/Textarea.svelte +75 -0
- package/dist/components/Textarea.svelte.d.ts +26 -0
- package/dist/components/ThemeToggle.svelte +33 -0
- package/dist/components/ThemeToggle.svelte.d.ts +6 -0
- package/dist/components/Toaster.svelte +54 -0
- package/dist/components/Toaster.svelte.d.ts +18 -0
- package/dist/components/Toggle.svelte +74 -0
- package/dist/components/Toggle.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +44 -0
- package/dist/components/Tooltip.svelte.d.ts +11 -0
- package/dist/components/dialog/ConfirmDialog.svelte +95 -0
- package/dist/components/dialog/ConfirmDialog.svelte.d.ts +8 -0
- package/dist/components/dialog/Dialog.svelte +225 -0
- package/dist/components/dialog/Dialog.svelte.d.ts +9 -0
- package/dist/components/dialog/DialogContainer.svelte +17 -0
- package/dist/components/dialog/DialogContainer.svelte.d.ts +18 -0
- package/dist/components/dialog/dialog-manager.svelte.d.ts +54 -0
- package/dist/components/dialog/dialog-manager.svelte.js +164 -0
- package/dist/components/dialog/types.d.ts +33 -0
- package/dist/components/dialog/types.js +10 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +72 -0
- package/dist/stores/toast.svelte.d.ts +18 -0
- package/dist/stores/toast.svelte.js +28 -0
- package/dist/styles/theme.css +148 -0
- package/dist/utils/cn.d.ts +13 -0
- package/dist/utils/cn.js +24 -0
- package/dist/utils/theme.d.ts +26 -0
- package/dist/utils/theme.js +55 -0
- package/package.json +81 -0
- package/src/lib/styles/theme.css +148 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { toast, type ToastTone } from '../stores/toast.svelte';
|
|
3
|
+
|
|
4
|
+
const styles: Record<ToastTone, { bar: string; icon: string; mark: string }> = {
|
|
5
|
+
info: { bar: 'bg-brand', icon: 'text-brand', mark: 'ℹ' },
|
|
6
|
+
success: { bar: 'bg-success', icon: 'text-success', mark: '✓' },
|
|
7
|
+
warn: { bar: 'bg-warn', icon: 'text-warn', mark: '!' },
|
|
8
|
+
danger: { bar: 'bg-danger', icon: 'text-danger', mark: '×' },
|
|
9
|
+
};
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div
|
|
13
|
+
class="pointer-events-none fixed bottom-4 right-4 z-[60] flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
|
|
14
|
+
>
|
|
15
|
+
{#each toast.items as t (t.id)}
|
|
16
|
+
<div
|
|
17
|
+
class="pointer-events-auto flex items-start gap-3 overflow-hidden rounded-lg border border-line
|
|
18
|
+
bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-in_140ms_ease-out]"
|
|
19
|
+
role="status"
|
|
20
|
+
>
|
|
21
|
+
<span class="w-1 self-stretch shrink-0 {styles[t.tone].bar}"></span>
|
|
22
|
+
<span
|
|
23
|
+
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current text-xs font-bold {styles[
|
|
24
|
+
t.tone
|
|
25
|
+
].icon}"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
>
|
|
28
|
+
{styles[t.tone].mark}
|
|
29
|
+
</span>
|
|
30
|
+
<p class="flex-1 text-sm text-fg leading-snug">{t.message}</p>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class="text-faint hover:text-fg transition-colors text-lg leading-none -mt-0.5"
|
|
34
|
+
aria-label="Dismiss"
|
|
35
|
+
onclick={() => toast.dismiss(t.id)}
|
|
36
|
+
>
|
|
37
|
+
×
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
@keyframes toast-in {
|
|
45
|
+
from {
|
|
46
|
+
opacity: 0;
|
|
47
|
+
transform: translateX(12px);
|
|
48
|
+
}
|
|
49
|
+
to {
|
|
50
|
+
opacity: 1;
|
|
51
|
+
transform: translateX(0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const Toaster: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type Toaster = InstanceType<typeof Toaster>;
|
|
18
|
+
export default Toaster;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
checked = $bindable(false),
|
|
4
|
+
label,
|
|
5
|
+
hint,
|
|
6
|
+
disabled = false,
|
|
7
|
+
size = 'md',
|
|
8
|
+
onchange,
|
|
9
|
+
}: {
|
|
10
|
+
checked?: boolean;
|
|
11
|
+
label?: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
size?: 'sm' | 'md';
|
|
15
|
+
onchange?: (checked: boolean) => void;
|
|
16
|
+
} = $props();
|
|
17
|
+
|
|
18
|
+
const track: Record<'sm' | 'md', string> = {
|
|
19
|
+
sm: 'w-8 h-4',
|
|
20
|
+
md: 'w-10 h-5',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const thumb: Record<'sm' | 'md', string> = {
|
|
24
|
+
sm: 'w-3 h-3 top-0.5 left-0.5',
|
|
25
|
+
md: 'w-4 h-4 top-0.5 left-0.5',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const thumbOn: Record<'sm' | 'md', string> = {
|
|
29
|
+
sm: 'translate-x-4',
|
|
30
|
+
md: 'translate-x-5',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function handleClick() {
|
|
34
|
+
if (disabled) return;
|
|
35
|
+
checked = !checked;
|
|
36
|
+
onchange?.(checked);
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<label
|
|
41
|
+
class="inline-flex items-start gap-2.5 cursor-pointer select-none {disabled
|
|
42
|
+
? 'opacity-50 cursor-not-allowed'
|
|
43
|
+
: ''}"
|
|
44
|
+
>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
role="switch"
|
|
48
|
+
aria-checked={checked}
|
|
49
|
+
aria-label={label ?? 'Toggle'}
|
|
50
|
+
{disabled}
|
|
51
|
+
onclick={handleClick}
|
|
52
|
+
class="relative shrink-0 rounded-full transition-colors focus:outline-none
|
|
53
|
+
focus-visible:ring-2 focus-visible:ring-brand/50 focus-visible:ring-offset-1
|
|
54
|
+
focus-visible:ring-offset-ink mt-0.5
|
|
55
|
+
{track[size]}
|
|
56
|
+
{checked ? 'bg-brand' : 'bg-surface-2'}"
|
|
57
|
+
>
|
|
58
|
+
<span
|
|
59
|
+
class="absolute rounded-full bg-fg shadow transition-transform
|
|
60
|
+
{thumb[size]}
|
|
61
|
+
{checked ? thumbOn[size] : 'translate-x-0'}"
|
|
62
|
+
></span>
|
|
63
|
+
</button>
|
|
64
|
+
{#if label || hint}
|
|
65
|
+
<span class="flex flex-col gap-0.5">
|
|
66
|
+
{#if label}
|
|
67
|
+
<span class="text-sm text-fg">{label}</span>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if hint}
|
|
70
|
+
<span class="text-xs text-faint">{hint}</span>
|
|
71
|
+
{/if}
|
|
72
|
+
</span>
|
|
73
|
+
{/if}
|
|
74
|
+
</label>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
checked?: boolean;
|
|
3
|
+
label?: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
size?: 'sm' | 'md';
|
|
7
|
+
onchange?: (checked: boolean) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "checked">;
|
|
10
|
+
type Toggle = ReturnType<typeof Toggle>;
|
|
11
|
+
export default Toggle;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Position = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
text: string;
|
|
8
|
+
position?: Position;
|
|
9
|
+
class?: string;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { text, position = 'top', class: cls = '', children }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let visible = $state(false);
|
|
16
|
+
|
|
17
|
+
const pos: Record<Position, string> = {
|
|
18
|
+
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
|
19
|
+
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
|
20
|
+
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
|
21
|
+
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
|
22
|
+
};
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
26
|
+
<span
|
|
27
|
+
class="relative inline-flex {cls}"
|
|
28
|
+
onmouseenter={() => (visible = true)}
|
|
29
|
+
onmouseleave={() => (visible = false)}
|
|
30
|
+
onfocus={() => (visible = true)}
|
|
31
|
+
onblur={() => (visible = false)}
|
|
32
|
+
>
|
|
33
|
+
{@render children()}
|
|
34
|
+
{#if visible}
|
|
35
|
+
<span
|
|
36
|
+
role="tooltip"
|
|
37
|
+
class="pointer-events-none absolute {pos[position]} z-50
|
|
38
|
+
whitespace-nowrap rounded-lg border border-line bg-surface-2
|
|
39
|
+
px-2.5 py-1.5 text-xs text-fg shadow-xl"
|
|
40
|
+
>
|
|
41
|
+
{text}
|
|
42
|
+
</span>
|
|
43
|
+
{/if}
|
|
44
|
+
</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Position = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
interface Props {
|
|
4
|
+
text: string;
|
|
5
|
+
position?: Position;
|
|
6
|
+
class?: string;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
declare const Tooltip: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Tooltip = ReturnType<typeof Tooltip>;
|
|
11
|
+
export default Tooltip;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ConfirmDialog - confirm/cancel pattern with async loading state.
|
|
4
|
+
*
|
|
5
|
+
* Renders a warning icon + message + Cancel/Confirm buttons.
|
|
6
|
+
* The confirm button shows a loading spinner during async actions.
|
|
7
|
+
*
|
|
8
|
+
* Usage (from DialogContainer):
|
|
9
|
+
* <Dialog {entry}>
|
|
10
|
+
* <ConfirmDialog {entry} />
|
|
11
|
+
* </Dialog>
|
|
12
|
+
*
|
|
13
|
+
* Programmatic usage:
|
|
14
|
+
* import { confirm } from '@lyeve/ui-kit';
|
|
15
|
+
* const ok = await confirm('Delete this item?', 'This action cannot be undone.');
|
|
16
|
+
* if (ok) { await deleteItem(); }
|
|
17
|
+
*/
|
|
18
|
+
import { TriangleAlert } from '@lucide/svelte';
|
|
19
|
+
import { closeDialog, dismissDialog } from './dialog-manager.svelte';
|
|
20
|
+
import Button from '../Button.svelte';
|
|
21
|
+
import type { DialogEntry } from './types.js';
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
entry,
|
|
25
|
+
/** If provided, called on confirm. Return false/throw to prevent close. */
|
|
26
|
+
onConfirm,
|
|
27
|
+
}: {
|
|
28
|
+
entry: DialogEntry<boolean>;
|
|
29
|
+
onConfirm?: () => Promise<boolean | void> | boolean | void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let loading = $state(false);
|
|
33
|
+
let title = $derived(
|
|
34
|
+
typeof entry.meta?.confirmTitle === 'string' ? entry.meta.confirmTitle : 'Confirm',
|
|
35
|
+
);
|
|
36
|
+
let message = $derived(
|
|
37
|
+
typeof entry.meta?.confirmMessage === 'string' ? entry.meta.confirmMessage : '',
|
|
38
|
+
);
|
|
39
|
+
let cancelLabel = $derived(
|
|
40
|
+
typeof entry.meta?.cancelLabel === 'string' ? entry.meta.cancelLabel : 'Cancel',
|
|
41
|
+
);
|
|
42
|
+
let confirmLabel = $derived(
|
|
43
|
+
typeof entry.meta?.confirmLabel === 'string' ? entry.meta.confirmLabel : 'Confirm',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
async function handleConfirm() {
|
|
47
|
+
if (loading) return;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
loading = true;
|
|
51
|
+
if (onConfirm) {
|
|
52
|
+
const result = await onConfirm();
|
|
53
|
+
if (result === false) {
|
|
54
|
+
loading = false;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
loading = false;
|
|
59
|
+
closeDialog(true, entry.id);
|
|
60
|
+
} catch {
|
|
61
|
+
loading = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleCancel() {
|
|
66
|
+
dismissDialog(entry.id);
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div class="flex flex-col gap-4">
|
|
71
|
+
<!-- Warning icon + message -->
|
|
72
|
+
<div class="flex items-start gap-3">
|
|
73
|
+
<div
|
|
74
|
+
class="flex items-center justify-center w-10 h-10 rounded-full bg-warn/10 text-warn shrink-0"
|
|
75
|
+
>
|
|
76
|
+
<TriangleAlert class="w-5 h-5" />
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex-1 min-w-0">
|
|
79
|
+
<p class="text-sm text-fg font-medium">{title}</p>
|
|
80
|
+
{#if message}
|
|
81
|
+
<p class="text-sm text-muted mt-1">{message}</p>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Actions -->
|
|
87
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
88
|
+
<Button variant="secondary" size="sm" onclick={handleCancel}>
|
|
89
|
+
{cancelLabel}
|
|
90
|
+
</Button>
|
|
91
|
+
<Button variant="danger" size="sm" onclick={handleConfirm} {loading}>
|
|
92
|
+
{confirmLabel}
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DialogEntry } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
entry: DialogEntry<boolean>;
|
|
4
|
+
onConfirm?: () => Promise<boolean | void> | boolean | void;
|
|
5
|
+
};
|
|
6
|
+
declare const ConfirmDialog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
|
|
8
|
+
export default ConfirmDialog;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Dialog - modal frame component.
|
|
4
|
+
*
|
|
5
|
+
* Handles: focus trap, ESC to close, click-outside to close,
|
|
6
|
+
* scale+fade animation, stacked z-indexing, portal to body.
|
|
7
|
+
*
|
|
8
|
+
* Rendered by DialogContainer for each entry in the dialog stack.
|
|
9
|
+
* Not meant to be used directly - use openDialog() from dialog-manager.
|
|
10
|
+
*/
|
|
11
|
+
import { X } from '@lucide/svelte';
|
|
12
|
+
import { onMount, tick } from 'svelte';
|
|
13
|
+
import type { Snippet } from 'svelte';
|
|
14
|
+
import type { DialogEntry } from './types.js';
|
|
15
|
+
import { sizeClass } from './types.js';
|
|
16
|
+
import {
|
|
17
|
+
closeDialog,
|
|
18
|
+
dismissDialog,
|
|
19
|
+
_lockBodyScroll,
|
|
20
|
+
_unlockBodyScroll,
|
|
21
|
+
} from './dialog-manager.svelte';
|
|
22
|
+
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
entry,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
entry: DialogEntry<any>;
|
|
29
|
+
children?: Snippet;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
// ──────────────────────────────────────────────────────
|
|
33
|
+
// Animation state
|
|
34
|
+
// ──────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
let visible = $state(false);
|
|
37
|
+
let exiting = $state(false);
|
|
38
|
+
|
|
39
|
+
// Stacked offset: each deeper dialog shrinks and shifts back
|
|
40
|
+
let offset = $derived(entry.depth);
|
|
41
|
+
let zIndex = $derived(50 + offset);
|
|
42
|
+
|
|
43
|
+
// ──────────────────────────────────────────────────────
|
|
44
|
+
// DOM refs
|
|
45
|
+
// ──────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
let dialogEl = $state<HTMLDivElement>();
|
|
48
|
+
let previousFocus = $state<HTMLElement | null>(null);
|
|
49
|
+
let focusableSelector =
|
|
50
|
+
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
51
|
+
|
|
52
|
+
// ──────────────────────────────────────────────────────
|
|
53
|
+
// Lifecycle
|
|
54
|
+
// ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
onMount(() => {
|
|
57
|
+
// Save previously focused element for restore on unmount
|
|
58
|
+
const el = document.activeElement;
|
|
59
|
+
previousFocus = el instanceof HTMLElement ? el : null;
|
|
60
|
+
|
|
61
|
+
// Trigger enter animation on next frame
|
|
62
|
+
requestAnimationFrame(() => {
|
|
63
|
+
visible = true;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Focus trap: focus first focusable element, or the dialog itself
|
|
67
|
+
tick().then(() => {
|
|
68
|
+
if (dialogEl) {
|
|
69
|
+
const focusable = dialogEl.querySelectorAll(focusableSelector);
|
|
70
|
+
if (focusable.length > 0) {
|
|
71
|
+
(focusable[0] as HTMLElement).focus();
|
|
72
|
+
} else {
|
|
73
|
+
dialogEl.focus();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Lock body scroll (counter-based: only unlock when no dialogs remain)
|
|
79
|
+
_lockBodyScroll();
|
|
80
|
+
const unlock = () => _unlockBodyScroll();
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
unlock();
|
|
84
|
+
// Restore focus
|
|
85
|
+
previousFocus?.focus?.();
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ──────────────────────────────────────────────────────
|
|
90
|
+
// Handlers
|
|
91
|
+
// ──────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
94
|
+
// Only close if clicking the backdrop itself (not the dialog panel)
|
|
95
|
+
if (e.target === e.currentTarget && !entry.options.persistent) {
|
|
96
|
+
handleDismiss();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
101
|
+
if (e.key === 'Escape') {
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
if (!entry.options.persistent) {
|
|
104
|
+
handleDismiss();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Focus trap
|
|
109
|
+
if (e.key === 'Tab' && dialogEl) {
|
|
110
|
+
const focusable = dialogEl.querySelectorAll(focusableSelector);
|
|
111
|
+
if (focusable.length === 0) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const first = focusable[0] as HTMLElement;
|
|
117
|
+
const last = focusable[focusable.length - 1] as HTMLElement;
|
|
118
|
+
|
|
119
|
+
if (e.shiftKey) {
|
|
120
|
+
if (document.activeElement === first) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
last.focus();
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (document.activeElement === last) {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
first.focus();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function handleDismiss(): Promise<void> {
|
|
134
|
+
exiting = true;
|
|
135
|
+
// Wait for exit animation
|
|
136
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
137
|
+
dismissDialog(entry.id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function handleClose(value?: unknown): Promise<void> {
|
|
141
|
+
exiting = true;
|
|
142
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
143
|
+
closeDialog(value, entry.id);
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
148
|
+
<div class="fixed inset-0 z-[{zIndex}] flex items-center justify-center" role="presentation">
|
|
149
|
+
<!-- Backdrop -->
|
|
150
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
151
|
+
<div
|
|
152
|
+
class="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-200"
|
|
153
|
+
class:opacity-0={!visible || exiting}
|
|
154
|
+
class:opacity-100={visible && !exiting}
|
|
155
|
+
onclick={handleBackdropClick}
|
|
156
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
157
|
+
if (e.key === 'Escape' && !entry.options.persistent) handleDismiss();
|
|
158
|
+
}}
|
|
159
|
+
role="presentation"
|
|
160
|
+
></div>
|
|
161
|
+
|
|
162
|
+
<!-- Dialog panel -->
|
|
163
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
164
|
+
<div
|
|
165
|
+
bind:this={dialogEl}
|
|
166
|
+
class="relative w-full {sizeClass(entry.options.size ?? 'md')} mx-4
|
|
167
|
+
bg-surface border border-line rounded-xl shadow-2xl
|
|
168
|
+
transition-all duration-200 ease-out
|
|
169
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
|
170
|
+
class:opacity-0={!visible || exiting}
|
|
171
|
+
class:opacity-100={visible && !exiting}
|
|
172
|
+
style="transform: scale({visible && !exiting ? 1 - offset * 0.03 : 0.95}) translateY({offset *
|
|
173
|
+
16}px);
|
|
174
|
+
transform-origin: center;"
|
|
175
|
+
role="dialog"
|
|
176
|
+
aria-modal="true"
|
|
177
|
+
aria-label={typeof entry.options.title === 'string' ? entry.options.title : 'Dialog'}
|
|
178
|
+
tabindex="-1"
|
|
179
|
+
onkeydown={handleKeydown}
|
|
180
|
+
>
|
|
181
|
+
<!-- Header -->
|
|
182
|
+
<div class="flex items-center justify-between px-6 pt-6 pb-2">
|
|
183
|
+
<div class="flex-1 min-w-0">
|
|
184
|
+
{#if entry.options.title && entry.meta?.confirmTitle === undefined}
|
|
185
|
+
{#if typeof entry.options.title === 'string'}
|
|
186
|
+
<h2 class="text-lg font-semibold text-fg truncate">
|
|
187
|
+
{entry.options.title}
|
|
188
|
+
</h2>
|
|
189
|
+
{:else}
|
|
190
|
+
{@render entry.options.title()}
|
|
191
|
+
{/if}
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{#if !entry.options.persistent}
|
|
196
|
+
<button
|
|
197
|
+
class="inline-flex items-center justify-center w-8 h-8 -mr-2 rounded-lg
|
|
198
|
+
text-muted hover:text-fg hover:bg-surface-2 transition-colors shrink-0"
|
|
199
|
+
onclick={() => handleClose()}
|
|
200
|
+
aria-label="Close"
|
|
201
|
+
>
|
|
202
|
+
<X class="w-4 h-4" />
|
|
203
|
+
</button>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- Body -->
|
|
208
|
+
<div class="px-6 py-2">
|
|
209
|
+
{#if entry.options.body}
|
|
210
|
+
{@render entry.options.body()}
|
|
211
|
+
{:else if entry.meta?.confirmTitle !== undefined}
|
|
212
|
+
<ConfirmDialog {entry} />
|
|
213
|
+
{:else if children}
|
|
214
|
+
{@render children()}
|
|
215
|
+
{/if}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<!-- Footer -->
|
|
219
|
+
{#if entry.options.footer}
|
|
220
|
+
<div class="flex items-center justify-end gap-3 px-6 pb-6 pt-2">
|
|
221
|
+
{@render entry.options.footer()}
|
|
222
|
+
</div>
|
|
223
|
+
{/if}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DialogEntry } from './types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
entry: DialogEntry<any>;
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const Dialog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Dialog = ReturnType<typeof Dialog>;
|
|
9
|
+
export default Dialog;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DialogContainer - renders the dialog stack.
|
|
4
|
+
*
|
|
5
|
+
* Mount once near the app root (in +layout.svelte).
|
|
6
|
+
* Reads from the module-level $state store - no props needed.
|
|
7
|
+
*
|
|
8
|
+
* Dialogs stack bottom-to-top; newest renders on top.
|
|
9
|
+
* Each dialog gets its own focus trap, ESC handler, and backdrop.
|
|
10
|
+
*/
|
|
11
|
+
import { getDialogStack } from './dialog-manager.svelte';
|
|
12
|
+
import Dialog from './Dialog.svelte';
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#each getDialogStack() as entry (entry.id)}
|
|
16
|
+
<Dialog {entry} />
|
|
17
|
+
{/each}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const DialogContainer: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type DialogContainer = InstanceType<typeof DialogContainer>;
|
|
18
|
+
export default DialogContainer;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog Manager - module-level Svelte 5 reactive state.
|
|
3
|
+
*
|
|
4
|
+
* Manages a stack of dialogs. Dialogs render in last-to-first order (newest on top).
|
|
5
|
+
* Each dialog returns a Promise that resolves/rejects when closed.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { openDialog } from '@lyeve/ui-kit';
|
|
9
|
+
* // Inside a .svelte.ts file or $effect:
|
|
10
|
+
* const result = await openDialog({ title: 'Delete?', body: mySnippet, size: 'sm' });
|
|
11
|
+
*
|
|
12
|
+
* For confirm/cancel pattern:
|
|
13
|
+
* import { confirm } from '@lyeve/ui-kit';
|
|
14
|
+
* const ok = await confirm('Delete this item?', 'This action cannot be undone.');
|
|
15
|
+
* if (ok) { /* ... *\/ }
|
|
16
|
+
*/
|
|
17
|
+
import type { DialogOptions, DialogEntry } from './types.js';
|
|
18
|
+
export declare function _lockBodyScroll(): void;
|
|
19
|
+
export declare function _unlockBodyScroll(): void;
|
|
20
|
+
/** Readonly snapshot for components */
|
|
21
|
+
export declare function getDialogStack(): readonly DialogEntry<any>[];
|
|
22
|
+
/**
|
|
23
|
+
* Open a new dialog. Returns a promise that resolves when the dialog is closed
|
|
24
|
+
* (via closeDialog with a value) or rejects if dismissed without a value.
|
|
25
|
+
*
|
|
26
|
+
* The dialog is pushed onto the stack and becomes the topmost.
|
|
27
|
+
*/
|
|
28
|
+
export declare function openDialog<T = void>(options: DialogOptions<T>): Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Close a dialog by id and resolve its promise with a value.
|
|
31
|
+
* If omitted, closes the topmost dialog.
|
|
32
|
+
*/
|
|
33
|
+
export declare function closeDialog<T = void>(value: T, id?: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Dismiss (cancel) a dialog - rejects the promise.
|
|
36
|
+
* Called when user clicks backdrop or presses ESC on a non-persistent dialog.
|
|
37
|
+
*/
|
|
38
|
+
export declare function dismissDialog(id?: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Close all open dialogs, rejecting their promises.
|
|
41
|
+
*/
|
|
42
|
+
export declare function dismissAllDialogs(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Convenience: confirm dialog.
|
|
45
|
+
* Returns `true` if user confirmed, `false` if cancelled.
|
|
46
|
+
*
|
|
47
|
+
* const ok = await confirm('Delete item?', 'This cannot be undone.');
|
|
48
|
+
*/
|
|
49
|
+
export declare function confirm(title: string, message?: string): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Set metadata on a dialog entry after it's opened.
|
|
52
|
+
* Used by convenience wrappers to pass extra data without polluting DialogOptions.
|
|
53
|
+
*/
|
|
54
|
+
export declare function setDialogMeta(id: string, meta: Record<string, unknown>): void;
|