@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,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
accept?: string;
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
onchange?: (files: FileList | null) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
id = undefined,
|
|
17
|
+
name = undefined,
|
|
18
|
+
accept = undefined,
|
|
19
|
+
multiple = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
error = undefined,
|
|
22
|
+
hint = undefined,
|
|
23
|
+
label = undefined,
|
|
24
|
+
class: cls = '',
|
|
25
|
+
onchange = undefined,
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let dragOver = $state(false);
|
|
29
|
+
|
|
30
|
+
function handleChange(e: Event & { currentTarget: HTMLInputElement }) {
|
|
31
|
+
onchange?.(e.currentTarget.files);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleDrop(e: DragEvent) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
dragOver = false;
|
|
37
|
+
if (disabled) return;
|
|
38
|
+
onchange?.(e.dataTransfer?.files ?? null);
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="flex flex-col gap-1.5 {cls}">
|
|
43
|
+
{#if label}
|
|
44
|
+
<label for={id} class="text-sm font-medium text-fg">{label}</label>
|
|
45
|
+
{/if}
|
|
46
|
+
|
|
47
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
48
|
+
<label
|
|
49
|
+
class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-xl
|
|
50
|
+
border-2 border-dashed px-4 py-8 text-center transition-colors
|
|
51
|
+
{error
|
|
52
|
+
? 'border-danger bg-danger/5 hover:bg-danger/8'
|
|
53
|
+
: dragOver
|
|
54
|
+
? 'border-brand bg-brand/8'
|
|
55
|
+
: 'border-line bg-surface-2/40 hover:border-brand/50 hover:bg-brand/5'}
|
|
56
|
+
{disabled ? 'cursor-not-allowed opacity-50' : ''}"
|
|
57
|
+
ondragover={(e) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
dragOver = true;
|
|
60
|
+
}}
|
|
61
|
+
ondragleave={() => (dragOver = false)}
|
|
62
|
+
ondrop={handleDrop}
|
|
63
|
+
>
|
|
64
|
+
<svg
|
|
65
|
+
width="24"
|
|
66
|
+
height="24"
|
|
67
|
+
viewBox="0 0 24 24"
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
stroke-width="1.5"
|
|
71
|
+
stroke-linecap="round"
|
|
72
|
+
stroke-linejoin="round"
|
|
73
|
+
class={error ? 'text-danger' : 'text-faint'}
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
>
|
|
76
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
77
|
+
<polyline points="17 8 12 3 7 8" />
|
|
78
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
79
|
+
</svg>
|
|
80
|
+
<div class="text-sm">
|
|
81
|
+
<span class="{error ? 'text-danger' : 'text-brand'} font-medium">Click to upload</span>
|
|
82
|
+
<span class="text-muted"> or drag & drop</span>
|
|
83
|
+
{#if accept}<p class="mt-0.5 text-xs text-faint">{accept}</p>{/if}
|
|
84
|
+
</div>
|
|
85
|
+
<input
|
|
86
|
+
type="file"
|
|
87
|
+
{id}
|
|
88
|
+
{name}
|
|
89
|
+
{accept}
|
|
90
|
+
{multiple}
|
|
91
|
+
{disabled}
|
|
92
|
+
onchange={handleChange}
|
|
93
|
+
class="sr-only"
|
|
94
|
+
/>
|
|
95
|
+
</label>
|
|
96
|
+
|
|
97
|
+
{#if error}
|
|
98
|
+
<p class="text-xs text-danger">{error}</p>
|
|
99
|
+
{:else if hint}
|
|
100
|
+
<p class="text-xs text-faint">{hint}</p>
|
|
101
|
+
{/if}
|
|
102
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
accept?: string;
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
hint?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
onchange?: (files: FileList | null) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const FileInput: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type FileInput = ReturnType<typeof FileInput>;
|
|
15
|
+
export default FileInput;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Tone = 'brand' | 'success' | 'warn' | 'danger' | 'neutral' | 'violet';
|
|
3
|
+
type Size = 'xs' | 'sm' | 'md' | 'lg';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
tone?: Tone;
|
|
7
|
+
pulse?: boolean;
|
|
8
|
+
size?: Size;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { tone = 'success', pulse = false, size = 'sm', class: cls = '' }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const tones: Record<Tone, string> = {
|
|
15
|
+
brand: 'bg-brand',
|
|
16
|
+
success: 'bg-success',
|
|
17
|
+
warn: 'bg-warn',
|
|
18
|
+
danger: 'bg-danger',
|
|
19
|
+
neutral: 'bg-muted',
|
|
20
|
+
violet: 'bg-violet',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const sizes: Record<Size, string> = {
|
|
24
|
+
xs: 'w-1.5 h-1.5',
|
|
25
|
+
sm: 'w-2 h-2',
|
|
26
|
+
md: 'w-2.5 h-2.5',
|
|
27
|
+
lg: 'w-3.5 h-3.5',
|
|
28
|
+
};
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<span class="relative inline-flex {cls}" aria-hidden="true">
|
|
32
|
+
{#if pulse}
|
|
33
|
+
<span
|
|
34
|
+
class="absolute inline-flex h-full w-full animate-ping rounded-full opacity-60 {tones[tone]}"
|
|
35
|
+
></span>
|
|
36
|
+
{/if}
|
|
37
|
+
<span class="relative inline-flex rounded-full {sizes[size]} {tones[tone]}"></span>
|
|
38
|
+
</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Tone = 'brand' | 'success' | 'warn' | 'danger' | 'neutral' | 'violet';
|
|
2
|
+
type Size = 'xs' | 'sm' | 'md' | 'lg';
|
|
3
|
+
interface Props {
|
|
4
|
+
tone?: Tone;
|
|
5
|
+
pulse?: boolean;
|
|
6
|
+
size?: Size;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const Indicator: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Indicator = ReturnType<typeof Indicator>;
|
|
11
|
+
export default Indicator;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
hint?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
value = $bindable(''),
|
|
19
|
+
label = undefined,
|
|
20
|
+
type = 'text',
|
|
21
|
+
placeholder = '',
|
|
22
|
+
hint = undefined,
|
|
23
|
+
error = undefined,
|
|
24
|
+
disabled = false,
|
|
25
|
+
required = false,
|
|
26
|
+
id = undefined,
|
|
27
|
+
class: klass = '',
|
|
28
|
+
...rest
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="flex flex-col gap-1.5 {klass}">
|
|
35
|
+
{#if label}
|
|
36
|
+
<label for={fieldId} class="text-sm font-medium text-fg">
|
|
37
|
+
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
38
|
+
</label>
|
|
39
|
+
{/if}
|
|
40
|
+
|
|
41
|
+
<input
|
|
42
|
+
id={fieldId}
|
|
43
|
+
{type}
|
|
44
|
+
{placeholder}
|
|
45
|
+
{disabled}
|
|
46
|
+
{required}
|
|
47
|
+
bind:value
|
|
48
|
+
aria-invalid={error ? 'true' : undefined}
|
|
49
|
+
class="w-full rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg placeholder:text-faint
|
|
50
|
+
transition-colors outline-none disabled:opacity-50 disabled:cursor-not-allowed
|
|
51
|
+
{error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
|
|
52
|
+
{...rest}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
{#if error}
|
|
56
|
+
<p class="text-xs text-danger">{error}</p>
|
|
57
|
+
{:else if hint}
|
|
58
|
+
<p class="text-xs text-faint">{hint}</p>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
value?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
declare const Input: import("svelte").Component<Props, {}, "value">;
|
|
16
|
+
type Input = ReturnType<typeof Input>;
|
|
17
|
+
export default Input;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { class: cls = '', children }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<kbd
|
|
13
|
+
class="inline-flex items-center justify-center rounded border border-line bg-surface-2
|
|
14
|
+
px-1.5 py-0.5 font-mono text-[11px] text-muted
|
|
15
|
+
shadow-[inset_0_-1px_0] shadow-line {cls}"
|
|
16
|
+
>
|
|
17
|
+
{@render children()}
|
|
18
|
+
</kbd>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
for: htmlFor,
|
|
6
|
+
required = false,
|
|
7
|
+
hint,
|
|
8
|
+
class: cls = '',
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
for?: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
hint?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
} = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<label
|
|
20
|
+
for={htmlFor}
|
|
21
|
+
class="flex items-center gap-1 text-xs font-medium text-muted uppercase tracking-wider {cls}"
|
|
22
|
+
>
|
|
23
|
+
{@render children()}
|
|
24
|
+
{#if required}
|
|
25
|
+
<span class="text-danger font-bold leading-none" aria-label="required">*</span>
|
|
26
|
+
{/if}
|
|
27
|
+
{#if hint}
|
|
28
|
+
<span class="text-faint normal-case font-normal ml-0.5">({hint})</span>
|
|
29
|
+
{/if}
|
|
30
|
+
</label>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
for?: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
hint?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
};
|
|
9
|
+
declare const Label: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type Label = ReturnType<typeof Label>;
|
|
11
|
+
export default Label;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
open?: boolean;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
size?: Size;
|
|
11
|
+
onclose?: () => void;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
footer?: Snippet;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
open = $bindable(false),
|
|
18
|
+
title = undefined,
|
|
19
|
+
description = undefined,
|
|
20
|
+
size = 'md',
|
|
21
|
+
onclose = undefined,
|
|
22
|
+
children,
|
|
23
|
+
footer,
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const widths: Record<Size, string> = {
|
|
27
|
+
sm: 'max-w-sm',
|
|
28
|
+
md: 'max-w-lg',
|
|
29
|
+
lg: 'max-w-2xl',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function close() {
|
|
33
|
+
open = false;
|
|
34
|
+
onclose?.();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function onkeydown(e: KeyboardEvent) {
|
|
38
|
+
if (e.key === 'Escape') {
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<svelte:window onkeydown={open ? onkeydown : undefined} />
|
|
46
|
+
|
|
47
|
+
{#if open}
|
|
48
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
|
|
52
|
+
aria-label="Close"
|
|
53
|
+
onclick={close}
|
|
54
|
+
></button>
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
class="relative w-full {widths[size]} rounded-xl border border-line bg-surface shadow-2xl
|
|
58
|
+
animate-[modal-in_120ms_ease-out]"
|
|
59
|
+
role="dialog"
|
|
60
|
+
aria-modal="true"
|
|
61
|
+
>
|
|
62
|
+
{#if title}
|
|
63
|
+
<div class="flex items-start justify-between gap-4 px-5 py-4 border-b border-line">
|
|
64
|
+
<div>
|
|
65
|
+
<h2 class="font-semibold text-fg">{title}</h2>
|
|
66
|
+
{#if description}<p class="text-sm text-muted mt-0.5">{description}</p>{/if}
|
|
67
|
+
</div>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onclick={close}
|
|
71
|
+
class="text-faint hover:text-fg transition-colors text-xl leading-none -mt-1"
|
|
72
|
+
aria-label="Close"
|
|
73
|
+
>
|
|
74
|
+
×
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
79
|
+
<div class="px-5 py-4">
|
|
80
|
+
{@render children()}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{#if footer}
|
|
84
|
+
<div
|
|
85
|
+
class="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface-2/40"
|
|
86
|
+
>
|
|
87
|
+
{@render footer()}
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
@keyframes modal-in {
|
|
96
|
+
from {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transform: translateY(6px) scale(0.98);
|
|
99
|
+
}
|
|
100
|
+
to {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
transform: translateY(0) scale(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
3
|
+
interface Props {
|
|
4
|
+
open?: boolean;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
size?: Size;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
footer?: Snippet;
|
|
11
|
+
}
|
|
12
|
+
declare const Modal: import("svelte").Component<Props, {}, "open">;
|
|
13
|
+
type Modal = ReturnType<typeof Modal>;
|
|
14
|
+
export default Modal;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// MultiSelect: pick several options, shown as removable chips, with an optional
|
|
3
|
+
// search box. Binds to a string[] of selected values.
|
|
4
|
+
interface Option {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
value = $bindable<string[]>([]),
|
|
12
|
+
options,
|
|
13
|
+
label = undefined,
|
|
14
|
+
hint = undefined,
|
|
15
|
+
error = undefined,
|
|
16
|
+
placeholder = 'Select…',
|
|
17
|
+
searchable = true,
|
|
18
|
+
disabled = false,
|
|
19
|
+
required = false,
|
|
20
|
+
id = undefined,
|
|
21
|
+
class: cls = '',
|
|
22
|
+
onchange = undefined,
|
|
23
|
+
}: {
|
|
24
|
+
value?: string[];
|
|
25
|
+
options: Option[];
|
|
26
|
+
label?: string;
|
|
27
|
+
hint?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
searchable?: boolean;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
required?: boolean;
|
|
33
|
+
id?: string;
|
|
34
|
+
class?: string;
|
|
35
|
+
onchange?: (value: string[]) => void;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
let open = $state(false);
|
|
39
|
+
let query = $state('');
|
|
40
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
41
|
+
|
|
42
|
+
const selected = $derived(options.filter((o) => value.includes(o.value)));
|
|
43
|
+
const filtered = $derived(
|
|
44
|
+
options.filter((o) => o.label.toLowerCase().includes(query.trim().toLowerCase())),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function isSelected(v: string) {
|
|
48
|
+
return value.includes(v);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toggle(opt: Option) {
|
|
52
|
+
if (opt.disabled) return;
|
|
53
|
+
value = isSelected(opt.value) ? value.filter((v) => v !== opt.value) : [...value, opt.value];
|
|
54
|
+
onchange?.(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function remove(v: string) {
|
|
58
|
+
value = value.filter((x) => x !== v);
|
|
59
|
+
onchange?.(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function open_() {
|
|
63
|
+
if (!disabled) open = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleOutside(e: MouseEvent) {
|
|
67
|
+
if (containerEl && !containerEl.contains(e.target as Node)) {
|
|
68
|
+
open = false;
|
|
69
|
+
query = '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function handleKey(e: KeyboardEvent) {
|
|
73
|
+
if (e.key === 'Escape') open = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$effect(() => {
|
|
77
|
+
if (open) {
|
|
78
|
+
document.addEventListener('click', handleOutside, { capture: true });
|
|
79
|
+
document.addEventListener('keydown', handleKey);
|
|
80
|
+
}
|
|
81
|
+
return () => {
|
|
82
|
+
document.removeEventListener('click', handleOutside, { capture: true });
|
|
83
|
+
document.removeEventListener('keydown', handleKey);
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<div class="flex flex-col gap-1.5 {cls}" bind:this={containerEl}>
|
|
89
|
+
{#if label}
|
|
90
|
+
<label for={id} class="text-sm font-medium text-fg">
|
|
91
|
+
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
92
|
+
</label>
|
|
93
|
+
{/if}
|
|
94
|
+
|
|
95
|
+
<div class="relative">
|
|
96
|
+
<!-- Trigger (a div, not a button, so the chip remove-buttons are valid children) -->
|
|
97
|
+
<div
|
|
98
|
+
{id}
|
|
99
|
+
role="button"
|
|
100
|
+
tabindex={disabled ? -1 : 0}
|
|
101
|
+
aria-haspopup="listbox"
|
|
102
|
+
aria-expanded={open}
|
|
103
|
+
aria-disabled={disabled}
|
|
104
|
+
onclick={() => (open ? (open = false) : open_())}
|
|
105
|
+
onkeydown={(e) => {
|
|
106
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
open ? (open = false) : open_();
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
class="w-full min-h-[2.375rem] flex flex-wrap items-center gap-1.5 rounded-lg border bg-surface-2
|
|
112
|
+
px-2.5 py-1.5 text-left text-sm transition-colors outline-none cursor-pointer
|
|
113
|
+
focus-visible:border-brand
|
|
114
|
+
{disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
115
|
+
{error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
|
|
116
|
+
>
|
|
117
|
+
{#if selected.length === 0}
|
|
118
|
+
<span class="text-faint px-0.5">{placeholder}</span>
|
|
119
|
+
{:else}
|
|
120
|
+
{#each selected as opt (opt.value)}
|
|
121
|
+
<span
|
|
122
|
+
class="inline-flex items-center gap-1 rounded-md bg-brand/10 text-brand px-2 py-0.5 text-xs"
|
|
123
|
+
>
|
|
124
|
+
{opt.label}
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
aria-label="Remove {opt.label}"
|
|
128
|
+
onclick={(e) => {
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
remove(opt.value);
|
|
131
|
+
}}
|
|
132
|
+
class="hover:text-brand-light leading-none">×</button
|
|
133
|
+
>
|
|
134
|
+
</span>
|
|
135
|
+
{/each}
|
|
136
|
+
{/if}
|
|
137
|
+
<span class="ml-auto shrink-0 text-faint" aria-hidden="true">
|
|
138
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
139
|
+
<path
|
|
140
|
+
d="M2 4l4 4 4-4"
|
|
141
|
+
stroke="currentColor"
|
|
142
|
+
stroke-width="1.5"
|
|
143
|
+
stroke-linecap="round"
|
|
144
|
+
stroke-linejoin="round"
|
|
145
|
+
/>
|
|
146
|
+
</svg>
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Panel -->
|
|
151
|
+
{#if open}
|
|
152
|
+
<div
|
|
153
|
+
role="listbox"
|
|
154
|
+
aria-multiselectable="true"
|
|
155
|
+
class="absolute z-50 mt-1 w-full rounded-xl border border-line bg-surface shadow-2xl overflow-hidden"
|
|
156
|
+
>
|
|
157
|
+
{#if searchable}
|
|
158
|
+
<div class="p-2 border-b border-line">
|
|
159
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
160
|
+
<input
|
|
161
|
+
autofocus
|
|
162
|
+
bind:value={query}
|
|
163
|
+
placeholder="Search…"
|
|
164
|
+
class="w-full rounded-md bg-surface-2 border border-line px-2.5 py-1.5 text-sm text-fg
|
|
165
|
+
placeholder:text-faint outline-none focus:border-brand"
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
169
|
+
<div class="max-h-60 overflow-y-auto py-1">
|
|
170
|
+
{#each filtered as opt (opt.value)}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
role="option"
|
|
174
|
+
aria-selected={isSelected(opt.value)}
|
|
175
|
+
disabled={opt.disabled}
|
|
176
|
+
onclick={() => toggle(opt)}
|
|
177
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-fg transition-colors
|
|
178
|
+
hover:bg-surface-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
179
|
+
>
|
|
180
|
+
<span
|
|
181
|
+
class="flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors
|
|
182
|
+
{isSelected(opt.value) ? 'bg-brand border-brand' : 'bg-surface-2 border-line'}"
|
|
183
|
+
>
|
|
184
|
+
{#if isSelected(opt.value)}
|
|
185
|
+
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" aria-hidden="true">
|
|
186
|
+
<path
|
|
187
|
+
d="M1 4l3 3 5-6"
|
|
188
|
+
stroke="currentColor"
|
|
189
|
+
stroke-width="1.5"
|
|
190
|
+
stroke-linecap="round"
|
|
191
|
+
stroke-linejoin="round"
|
|
192
|
+
class="text-ink"
|
|
193
|
+
/>
|
|
194
|
+
</svg>
|
|
195
|
+
{/if}
|
|
196
|
+
</span>
|
|
197
|
+
<span class="text-left">{opt.label}</span>
|
|
198
|
+
</button>
|
|
199
|
+
{:else}
|
|
200
|
+
<p class="px-3 py-2 text-sm text-faint">No matches</p>
|
|
201
|
+
{/each}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{#if error}
|
|
208
|
+
<p class="text-xs text-danger">{error}</p>
|
|
209
|
+
{:else if hint}
|
|
210
|
+
<p class="text-xs text-faint">{hint}</p>
|
|
211
|
+
{/if}
|
|
212
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface Option {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
value?: string[];
|
|
8
|
+
options: Option[];
|
|
9
|
+
label?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
searchable?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
id?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
onchange?: (value: string[]) => void;
|
|
19
|
+
};
|
|
20
|
+
declare const MultiSelect: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
21
|
+
type MultiSelect = ReturnType<typeof MultiSelect>;
|
|
22
|
+
export default MultiSelect;
|