@r2digisolutions/components 0.5.0 → 0.6.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/dist/components/atoms/Button/Button.stories.d.ts +6 -0
- package/dist/components/atoms/Button/Button.stories.js +7 -0
- package/dist/components/atoms/Button/Button.svelte +15 -17
- package/dist/components/atoms/Button/Button.svelte.d.ts +1 -0
- package/dist/components/atoms/Button/ButtonStory.svelte +2 -0
- package/dist/components/atoms/Button/ButtonStory.svelte.d.ts +1 -0
- package/dist/components/molecules/Combobox/Combobox.stories.d.ts +6 -0
- package/dist/components/molecules/Combobox/Combobox.stories.js +5 -2
- package/dist/components/molecules/Combobox/Combobox.svelte +155 -64
- package/dist/components/molecules/Combobox/Combobox.svelte.d.ts +15 -0
- package/dist/components/molecules/Combobox/ComboboxStory.svelte +25 -12
- package/dist/components/molecules/Combobox/ComboboxStory.svelte.d.ts +1 -0
- package/dist/components/molecules/Combobox/combobox-context.d.ts +12 -0
- package/dist/components/molecules/Combobox/combobox-context.js +10 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.stories.d.ts +9 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.stories.js +8 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.svelte +121 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItem.svelte.d.ts +33 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItemStory.svelte +54 -0
- package/dist/components/molecules/ComboboxItem/ComboboxItemStory.svelte.d.ts +3 -0
- package/dist/components/molecules/Form/Form.svelte +36 -10
- package/dist/components/molecules/FormField/FormField.svelte +56 -19
- package/dist/components/molecules/FormField/FormField.svelte.d.ts +21 -6
- package/dist/components/molecules/FormPasswordInput/FormPasswordInput.svelte +38 -9
- package/dist/components/molecules/FormPasswordInput/FormPasswordInput.svelte.d.ts +8 -1
- package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte +1 -1
- package/dist/components/organisms/FileUploader/FileUploader.svelte +7 -3
- package/dist/components/organisms/InvoicePage/InvoicePage.svelte +1 -1
- package/dist/components/organisms/ProfileHeaderUploader/ProfileHeaderUploader.svelte +30 -10
- package/dist/components/organisms/ProfileHeaderUploader/ProfileHeaderUploader.svelte.d.ts +4 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/styles.css +6 -0
- package/dist/utils/formContext.d.ts +58 -0
- package/dist/utils/formContext.js +79 -1
- package/package.json +16 -16
|
@@ -7,6 +7,7 @@ declare const meta: {
|
|
|
7
7
|
loading?: boolean;
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
fullWidth?: boolean;
|
|
10
|
+
href?: string;
|
|
10
11
|
label?: string;
|
|
11
12
|
}, {}, "">;
|
|
12
13
|
tags: string[];
|
|
@@ -33,6 +34,10 @@ declare const meta: {
|
|
|
33
34
|
control: "boolean";
|
|
34
35
|
description: string;
|
|
35
36
|
};
|
|
37
|
+
href: {
|
|
38
|
+
control: "text";
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
36
41
|
label: {
|
|
37
42
|
control: "text";
|
|
38
43
|
description: string;
|
|
@@ -59,3 +64,4 @@ export declare const Disabled: Story;
|
|
|
59
64
|
export declare const ExtraSmall: Story;
|
|
60
65
|
export declare const Large: Story;
|
|
61
66
|
export declare const FullWidth: Story;
|
|
67
|
+
export declare const AsLink: Story;
|
|
@@ -26,6 +26,10 @@ const meta = {
|
|
|
26
26
|
control: 'boolean',
|
|
27
27
|
description: 'Makes the button take full container width'
|
|
28
28
|
},
|
|
29
|
+
href: {
|
|
30
|
+
control: 'text',
|
|
31
|
+
description: 'If set, renders as an anchor instead of a button'
|
|
32
|
+
},
|
|
29
33
|
label: {
|
|
30
34
|
control: 'text',
|
|
31
35
|
description: 'Button text content'
|
|
@@ -71,3 +75,6 @@ export const Large = {
|
|
|
71
75
|
export const FullWidth = {
|
|
72
76
|
args: { fullWidth: true, label: 'Full Width' }
|
|
73
77
|
};
|
|
78
|
+
export const AsLink = {
|
|
79
|
+
args: { href: '#', label: 'Go to page' }
|
|
80
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
loading?: boolean;
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
fullWidth?: boolean;
|
|
13
|
+
href?: string;
|
|
13
14
|
type?: 'button' | 'submit' | 'reset';
|
|
14
15
|
class?: string;
|
|
15
16
|
children?: Snippet;
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
loading = false,
|
|
24
25
|
disabled = false,
|
|
25
26
|
fullWidth = false,
|
|
27
|
+
href,
|
|
26
28
|
type = 'button',
|
|
27
29
|
class: className = '',
|
|
28
30
|
children,
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
}: ButtonProps = $props();
|
|
32
34
|
|
|
33
35
|
const isDisabled = $derived(disabled || loading);
|
|
36
|
+
const tag = $derived(href ? 'a' : 'button');
|
|
34
37
|
|
|
35
38
|
const baseClasses =
|
|
36
39
|
'relative inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 select-none cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 overflow-hidden';
|
|
@@ -65,24 +68,28 @@
|
|
|
65
68
|
};
|
|
66
69
|
</script>
|
|
67
70
|
|
|
68
|
-
<
|
|
69
|
-
{
|
|
71
|
+
<svelte:element
|
|
72
|
+
this={tag}
|
|
73
|
+
href={isDisabled ? undefined : href}
|
|
74
|
+
type={href ? undefined : type}
|
|
75
|
+
disabled={href ? undefined : isDisabled}
|
|
76
|
+
aria-disabled={href && isDisabled ? true : undefined}
|
|
77
|
+
aria-busy={loading}
|
|
70
78
|
class={[
|
|
71
79
|
baseClasses,
|
|
72
80
|
variantClasses[variant],
|
|
73
81
|
sizeClasses[size],
|
|
74
82
|
(size === 'md' || size === 'lg' || size === 'xl') && 'touch-target',
|
|
75
83
|
fullWidth && 'w-full',
|
|
84
|
+
href && isDisabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
|
76
85
|
className
|
|
77
86
|
]}
|
|
78
|
-
disabled={isDisabled}
|
|
79
|
-
aria-busy={loading}
|
|
80
87
|
{onclick}
|
|
81
88
|
{...rest}
|
|
82
89
|
>
|
|
83
90
|
{#if loading}
|
|
84
91
|
<!-- Spinner overlay -->
|
|
85
|
-
<span class="
|
|
92
|
+
<span class="inset-0 absolute flex items-center justify-center">
|
|
86
93
|
<svg
|
|
87
94
|
class={['animate-spin', iconSizeClasses[size]]}
|
|
88
95
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -90,18 +97,9 @@
|
|
|
90
97
|
viewBox="0 0 24 24"
|
|
91
98
|
aria-hidden="true"
|
|
92
99
|
>
|
|
93
|
-
<circle
|
|
94
|
-
class="opacity-25"
|
|
95
|
-
cx="12"
|
|
96
|
-
cy="12"
|
|
97
|
-
r="10"
|
|
98
|
-
stroke="currentColor"
|
|
99
|
-
stroke-width="4"
|
|
100
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
100
101
|
></circle>
|
|
101
|
-
<path
|
|
102
|
-
class="opacity-75"
|
|
103
|
-
fill="currentColor"
|
|
104
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
102
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
105
103
|
></path>
|
|
106
104
|
</svg>
|
|
107
105
|
</span>
|
|
@@ -112,4 +110,4 @@
|
|
|
112
110
|
{:else}
|
|
113
111
|
{@render children?.()}
|
|
114
112
|
{/if}
|
|
115
|
-
</
|
|
113
|
+
</svelte:element>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
loading?: boolean;
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
fullWidth?: boolean;
|
|
10
|
+
href?: string;
|
|
10
11
|
label?: string;
|
|
11
12
|
}>();
|
|
12
13
|
</script>
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
loading={props.loading ?? false}
|
|
19
20
|
disabled={props.disabled ?? false}
|
|
20
21
|
fullWidth={props.fullWidth ?? false}
|
|
22
|
+
href={props.href}
|
|
21
23
|
>
|
|
22
24
|
{props.label ?? 'Button'}
|
|
23
25
|
</Button>
|
|
@@ -5,6 +5,7 @@ declare const meta: {
|
|
|
5
5
|
creatable?: boolean;
|
|
6
6
|
label?: string;
|
|
7
7
|
disabled?: boolean;
|
|
8
|
+
compound?: boolean;
|
|
8
9
|
}, {}, "">;
|
|
9
10
|
tags: string[];
|
|
10
11
|
argTypes: {
|
|
@@ -14,11 +15,15 @@ declare const meta: {
|
|
|
14
15
|
disabled: {
|
|
15
16
|
control: "boolean";
|
|
16
17
|
};
|
|
18
|
+
compound: {
|
|
19
|
+
control: "boolean";
|
|
20
|
+
};
|
|
17
21
|
};
|
|
18
22
|
args: {
|
|
19
23
|
creatable: false;
|
|
20
24
|
label: string;
|
|
21
25
|
disabled: false;
|
|
26
|
+
compound: false;
|
|
22
27
|
};
|
|
23
28
|
};
|
|
24
29
|
export default meta;
|
|
@@ -26,3 +31,4 @@ type Story = StoryObj<typeof meta>;
|
|
|
26
31
|
export declare const Default: Story;
|
|
27
32
|
export declare const Creatable: Story;
|
|
28
33
|
export declare const Disabled: Story;
|
|
34
|
+
export declare const WithComboboxItem: Story;
|
|
@@ -5,15 +5,18 @@ const meta = {
|
|
|
5
5
|
tags: ['autodocs'],
|
|
6
6
|
argTypes: {
|
|
7
7
|
creatable: { control: 'boolean' },
|
|
8
|
-
disabled: { control: 'boolean' }
|
|
8
|
+
disabled: { control: 'boolean' },
|
|
9
|
+
compound: { control: 'boolean' }
|
|
9
10
|
},
|
|
10
11
|
args: {
|
|
11
12
|
creatable: false,
|
|
12
13
|
label: 'Framework',
|
|
13
|
-
disabled: false
|
|
14
|
+
disabled: false,
|
|
15
|
+
compound: false
|
|
14
16
|
}
|
|
15
17
|
};
|
|
16
18
|
export default meta;
|
|
17
19
|
export const Default = {};
|
|
18
20
|
export const Creatable = { args: { creatable: true } };
|
|
19
21
|
export const Disabled = { args: { disabled: true } };
|
|
22
|
+
export const WithComboboxItem = { args: { compound: true } };
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import ComboboxItem from '../ComboboxItem/ComboboxItem.svelte';
|
|
4
|
+
import {
|
|
5
|
+
COMBOBOX_CREATE_KEY,
|
|
6
|
+
setComboboxContext,
|
|
7
|
+
type ComboboxContext
|
|
8
|
+
} from './combobox-context.js';
|
|
9
|
+
|
|
2
10
|
export interface ComboboxOption {
|
|
3
11
|
value: string;
|
|
4
12
|
label: string;
|
|
5
13
|
disabled?: boolean;
|
|
6
14
|
}
|
|
7
15
|
|
|
16
|
+
export interface ComboboxItemState {
|
|
17
|
+
selected: boolean;
|
|
18
|
+
highlighted: boolean;
|
|
19
|
+
index: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
interface ComboboxProps {
|
|
9
23
|
options?: ComboboxOption[];
|
|
10
24
|
value?: string;
|
|
@@ -19,6 +33,15 @@
|
|
|
19
33
|
emptyText?: string;
|
|
20
34
|
createText?: (query: string) => string;
|
|
21
35
|
class?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Custom row for each `options` entry. If omitted, rows use `ComboboxItem`.
|
|
38
|
+
*/
|
|
39
|
+
item?: Snippet<[ComboboxOption, ComboboxItemState]>;
|
|
40
|
+
/**
|
|
41
|
+
* Extra (or fully custom) `ComboboxItem` rows rendered in the listbox.
|
|
42
|
+
* Nested items register for keyboard nav and filter against `query`.
|
|
43
|
+
*/
|
|
44
|
+
children?: Snippet;
|
|
22
45
|
onchange?: (value: string) => void;
|
|
23
46
|
oncreate?: (value: string) => void;
|
|
24
47
|
}
|
|
@@ -35,6 +58,8 @@
|
|
|
35
58
|
emptyText = 'No results',
|
|
36
59
|
createText = (q) => `Create “${q}”`,
|
|
37
60
|
class: className = '',
|
|
61
|
+
item: itemSnippet,
|
|
62
|
+
children,
|
|
38
63
|
onchange,
|
|
39
64
|
oncreate
|
|
40
65
|
}: ComboboxProps = $props();
|
|
@@ -42,6 +67,10 @@
|
|
|
42
67
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
43
68
|
let inputEl = $state<HTMLInputElement | null>(null);
|
|
44
69
|
let highlighted = $state(0);
|
|
70
|
+
let childEntries = $state<{ id: number; value: string; disabled: boolean; label: string }[]>([]);
|
|
71
|
+
let nextChildId = 0;
|
|
72
|
+
const uid = $props.id();
|
|
73
|
+
const listboxId = `${uid}-listbox`;
|
|
45
74
|
|
|
46
75
|
const selected = $derived(options.find((o) => o.value === value));
|
|
47
76
|
|
|
@@ -61,12 +90,33 @@
|
|
|
61
90
|
});
|
|
62
91
|
|
|
63
92
|
const items = $derived.by(() => {
|
|
64
|
-
const list: Array<
|
|
65
|
-
|
|
93
|
+
const list: Array<
|
|
94
|
+
{ kind: 'option'; option: ComboboxOption } | { kind: 'create'; label: string }
|
|
95
|
+
> = filtered.map((option) => ({ kind: 'option' as const, option }));
|
|
66
96
|
if (canCreate) list.push({ kind: 'create', label: createText(query.trim()) });
|
|
67
97
|
return list;
|
|
68
98
|
});
|
|
69
99
|
|
|
100
|
+
function itemKey(
|
|
101
|
+
item: { kind: 'option'; option: ComboboxOption } | { kind: 'create'; label: string }
|
|
102
|
+
): string {
|
|
103
|
+
return item.kind === 'create' ? COMBOBOX_CREATE_KEY : item.option.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const navKeys = $derived.by(() => {
|
|
107
|
+
const keys = items.map(itemKey);
|
|
108
|
+
for (const entry of childEntries) {
|
|
109
|
+
if (!entry.disabled && !keys.includes(entry.value)) keys.push(entry.value);
|
|
110
|
+
}
|
|
111
|
+
return keys;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const activeIndex = $derived(
|
|
115
|
+
navKeys.length === 0 ? 0 : Math.min(highlighted, navKeys.length - 1)
|
|
116
|
+
);
|
|
117
|
+
const highlightedKey = $derived(navKeys[activeIndex] ?? null);
|
|
118
|
+
const hasRows = $derived(items.length > 0 || childEntries.length > 0 || Boolean(children));
|
|
119
|
+
|
|
70
120
|
function setOpen(next: boolean) {
|
|
71
121
|
if (disabled) return;
|
|
72
122
|
open = next;
|
|
@@ -93,6 +143,28 @@
|
|
|
93
143
|
setOpen(false);
|
|
94
144
|
}
|
|
95
145
|
|
|
146
|
+
function selectByKey(key: string) {
|
|
147
|
+
if (key === COMBOBOX_CREATE_KEY) {
|
|
148
|
+
createOption();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const option = options.find((o) => o.value === key);
|
|
152
|
+
if (option) {
|
|
153
|
+
selectOption(option);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const child = childEntries.find((entry) => entry.value === key);
|
|
157
|
+
value = key;
|
|
158
|
+
query = child?.label || key;
|
|
159
|
+
onchange?.(key);
|
|
160
|
+
setOpen(false);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function highlightByKey(key: string) {
|
|
164
|
+
const index = navKeys.indexOf(key);
|
|
165
|
+
if (index >= 0) highlighted = index;
|
|
166
|
+
}
|
|
167
|
+
|
|
96
168
|
function onInput() {
|
|
97
169
|
setOpen(true);
|
|
98
170
|
highlighted = 0;
|
|
@@ -113,21 +185,20 @@
|
|
|
113
185
|
if (e.key === 'ArrowDown') {
|
|
114
186
|
e.preventDefault();
|
|
115
187
|
if (!open) setOpen(true);
|
|
116
|
-
else if (
|
|
188
|
+
else if (navKeys.length) highlighted = (activeIndex + 1) % navKeys.length;
|
|
117
189
|
return;
|
|
118
190
|
}
|
|
119
191
|
if (e.key === 'ArrowUp') {
|
|
120
192
|
e.preventDefault();
|
|
121
193
|
if (!open) setOpen(true);
|
|
122
|
-
else if (
|
|
194
|
+
else if (navKeys.length) highlighted = (activeIndex - 1 + navKeys.length) % navKeys.length;
|
|
123
195
|
return;
|
|
124
196
|
}
|
|
125
197
|
if (e.key === 'Enter') {
|
|
126
198
|
e.preventDefault();
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
else selectOption(item.option);
|
|
199
|
+
const key = navKeys[activeIndex];
|
|
200
|
+
if (!key) return;
|
|
201
|
+
selectByKey(key);
|
|
131
202
|
return;
|
|
132
203
|
}
|
|
133
204
|
if (e.key === 'Escape') {
|
|
@@ -150,24 +221,44 @@
|
|
|
150
221
|
query = selected.label;
|
|
151
222
|
}
|
|
152
223
|
});
|
|
224
|
+
|
|
225
|
+
const ctx: ComboboxContext = {
|
|
226
|
+
getValue: () => value,
|
|
227
|
+
getQuery: () => query,
|
|
228
|
+
getHighlighted: () => highlightedKey,
|
|
229
|
+
select: selectByKey,
|
|
230
|
+
highlight: highlightByKey,
|
|
231
|
+
register: (childValue, childDisabled, childLabel = '') => {
|
|
232
|
+
const id = ++nextChildId;
|
|
233
|
+
childEntries = [
|
|
234
|
+
...childEntries,
|
|
235
|
+
{ id, value: childValue, disabled: childDisabled, label: childLabel }
|
|
236
|
+
];
|
|
237
|
+
return () => {
|
|
238
|
+
childEntries = childEntries.filter((entry) => entry.id !== id);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
setComboboxContext(ctx);
|
|
153
244
|
</script>
|
|
154
245
|
|
|
155
246
|
<svelte:document onpointerdown={onDocPointerDown} />
|
|
156
247
|
|
|
157
|
-
<div class={['relative w-full
|
|
248
|
+
<div class={['max-w-sm relative w-full', className]} bind:this={rootEl}>
|
|
158
249
|
{#if label}
|
|
159
|
-
<span class="mb-1.5
|
|
250
|
+
<span class="mb-1.5 text-sm font-medium text-primary block">{label}</span>
|
|
160
251
|
{/if}
|
|
161
252
|
|
|
162
253
|
<div
|
|
163
254
|
class={[
|
|
164
|
-
'
|
|
165
|
-
open && 'border-brand-500 ring-
|
|
255
|
+
'h-10 gap-2 rounded-xl border-border bg-surface-elevated px-3 flex items-center border transition-colors',
|
|
256
|
+
open && 'border-brand-500 ring-brand-500/20 ring-2',
|
|
166
257
|
disabled && 'cursor-not-allowed opacity-60'
|
|
167
258
|
]}
|
|
168
259
|
>
|
|
169
260
|
<svg
|
|
170
|
-
class="h-4 w-4 shrink-0
|
|
261
|
+
class="h-4 w-4 text-muted shrink-0"
|
|
171
262
|
viewBox="0 0 24 24"
|
|
172
263
|
fill="none"
|
|
173
264
|
stroke="currentColor"
|
|
@@ -187,12 +278,13 @@
|
|
|
187
278
|
{disabled}
|
|
188
279
|
role="combobox"
|
|
189
280
|
aria-expanded={open}
|
|
281
|
+
aria-controls={listboxId}
|
|
190
282
|
aria-autocomplete="list"
|
|
191
283
|
autocomplete="off"
|
|
192
284
|
oninput={onInput}
|
|
193
285
|
onfocus={onFocus}
|
|
194
286
|
onkeydown={onKeydown}
|
|
195
|
-
class="min-w-0
|
|
287
|
+
class="min-w-0 text-sm text-primary placeholder:text-muted flex-1 bg-transparent outline-none"
|
|
196
288
|
/>
|
|
197
289
|
{#if query && !disabled}
|
|
198
290
|
<button
|
|
@@ -207,7 +299,13 @@
|
|
|
207
299
|
}}
|
|
208
300
|
class="text-muted hover:text-primary"
|
|
209
301
|
>
|
|
210
|
-
<svg
|
|
302
|
+
<svg
|
|
303
|
+
class="h-3.5 w-3.5"
|
|
304
|
+
viewBox="0 0 24 24"
|
|
305
|
+
fill="none"
|
|
306
|
+
stroke="currentColor"
|
|
307
|
+
stroke-width="2"
|
|
308
|
+
>
|
|
211
309
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
212
310
|
</svg>
|
|
213
311
|
</button>
|
|
@@ -216,67 +314,60 @@
|
|
|
216
314
|
|
|
217
315
|
{#if open}
|
|
218
316
|
<div
|
|
317
|
+
id={listboxId}
|
|
219
318
|
role="listbox"
|
|
220
|
-
class="
|
|
319
|
+
class="left-0 right-0 mt-2 max-h-60 rounded-xl border-border bg-surface-elevated p-1.5 shadow-xl absolute z-50 overflow-y-auto border"
|
|
221
320
|
>
|
|
222
|
-
{#if
|
|
223
|
-
<div class="px-3 py-2.5 text-
|
|
321
|
+
{#if !hasRows}
|
|
322
|
+
<div class="px-3 py-2.5 text-xs text-muted text-center">{emptyText}</div>
|
|
224
323
|
{:else}
|
|
225
|
-
{#each items as item, index (item
|
|
324
|
+
{#each items as item, index (itemKey(item))}
|
|
226
325
|
{#if item.kind === 'create'}
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
326
|
+
<ComboboxItem
|
|
327
|
+
value={COMBOBOX_CREATE_KEY}
|
|
328
|
+
label={item.label}
|
|
329
|
+
highlighted={activeIndex === index}
|
|
330
|
+
register={false}
|
|
232
331
|
onclick={createOption}
|
|
233
|
-
class={[
|
|
234
|
-
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm',
|
|
235
|
-
highlighted === index ? 'bg-brand-500 text-white' : 'text-primary hover:bg-surface-overlay'
|
|
236
|
-
]}
|
|
237
332
|
>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
333
|
+
{#snippet leading()}
|
|
334
|
+
<svg
|
|
335
|
+
class="h-4 w-4 shrink-0"
|
|
336
|
+
viewBox="0 0 24 24"
|
|
337
|
+
fill="none"
|
|
338
|
+
stroke="currentColor"
|
|
339
|
+
stroke-width="2"
|
|
340
|
+
aria-hidden="true"
|
|
341
|
+
>
|
|
342
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
|
343
|
+
</svg>
|
|
344
|
+
{/snippet}
|
|
248
345
|
{item.label}
|
|
249
|
-
</
|
|
346
|
+
</ComboboxItem>
|
|
250
347
|
{:else}
|
|
251
348
|
{@const option = item.option}
|
|
252
349
|
{@const isSelected = value === option.value}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
option.disabled
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
<span class="min-w-0 flex-1 truncate">{option.label}</span>
|
|
272
|
-
{#if isSelected}
|
|
273
|
-
<svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
274
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
275
|
-
</svg>
|
|
276
|
-
{/if}
|
|
277
|
-
</button>
|
|
350
|
+
{@const isHighlighted = activeIndex === index}
|
|
351
|
+
{#if itemSnippet}
|
|
352
|
+
{@render itemSnippet(option, {
|
|
353
|
+
selected: isSelected,
|
|
354
|
+
highlighted: isHighlighted,
|
|
355
|
+
index
|
|
356
|
+
})}
|
|
357
|
+
{:else}
|
|
358
|
+
<ComboboxItem
|
|
359
|
+
value={option.value}
|
|
360
|
+
label={option.label}
|
|
361
|
+
disabled={option.disabled}
|
|
362
|
+
selected={isSelected}
|
|
363
|
+
highlighted={isHighlighted}
|
|
364
|
+
register={false}
|
|
365
|
+
onclick={() => selectOption(option)}
|
|
366
|
+
/>
|
|
367
|
+
{/if}
|
|
278
368
|
{/if}
|
|
279
369
|
{/each}
|
|
370
|
+
{@render children?.()}
|
|
280
371
|
{/if}
|
|
281
372
|
</div>
|
|
282
373
|
{/if}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
export interface ComboboxOption {
|
|
2
3
|
value: string;
|
|
3
4
|
label: string;
|
|
4
5
|
disabled?: boolean;
|
|
5
6
|
}
|
|
7
|
+
export interface ComboboxItemState {
|
|
8
|
+
selected: boolean;
|
|
9
|
+
highlighted: boolean;
|
|
10
|
+
index: number;
|
|
11
|
+
}
|
|
6
12
|
interface ComboboxProps {
|
|
7
13
|
options?: ComboboxOption[];
|
|
8
14
|
value?: string;
|
|
@@ -17,6 +23,15 @@ interface ComboboxProps {
|
|
|
17
23
|
emptyText?: string;
|
|
18
24
|
createText?: (query: string) => string;
|
|
19
25
|
class?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Custom row for each `options` entry. If omitted, rows use `ComboboxItem`.
|
|
28
|
+
*/
|
|
29
|
+
item?: Snippet<[ComboboxOption, ComboboxItemState]>;
|
|
30
|
+
/**
|
|
31
|
+
* Extra (or fully custom) `ComboboxItem` rows rendered in the listbox.
|
|
32
|
+
* Nested items register for keyboard nav and filter against `query`.
|
|
33
|
+
*/
|
|
34
|
+
children?: Snippet;
|
|
20
35
|
onchange?: (value: string) => void;
|
|
21
36
|
oncreate?: (value: string) => void;
|
|
22
37
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Combobox, { type ComboboxOption } from './Combobox.svelte';
|
|
3
|
+
import ComboboxItem from '../ComboboxItem/ComboboxItem.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
creatable = false,
|
|
6
7
|
label = 'Framework',
|
|
7
|
-
disabled = false
|
|
8
|
+
disabled = false,
|
|
9
|
+
compound = false
|
|
8
10
|
}: {
|
|
9
11
|
creatable?: boolean;
|
|
10
12
|
label?: string;
|
|
11
13
|
disabled?: boolean;
|
|
14
|
+
compound?: boolean;
|
|
12
15
|
} = $props();
|
|
13
16
|
|
|
14
17
|
const options: ComboboxOption[] = [
|
|
@@ -25,17 +28,27 @@
|
|
|
25
28
|
let open = $state(false);
|
|
26
29
|
</script>
|
|
27
30
|
|
|
28
|
-
<div class="
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
<div class="w-80 gap-3 flex flex-col">
|
|
32
|
+
{#if compound}
|
|
33
|
+
<Combobox {label} {disabled} bind:value bind:query bind:open placeholder="Search frameworks…">
|
|
34
|
+
{#each options as option (option.value)}
|
|
35
|
+
<ComboboxItem value={option.value} label={option.label} disabled={option.disabled}>
|
|
36
|
+
{option.label}
|
|
37
|
+
</ComboboxItem>
|
|
38
|
+
{/each}
|
|
39
|
+
</Combobox>
|
|
40
|
+
{:else}
|
|
41
|
+
<Combobox
|
|
42
|
+
{options}
|
|
43
|
+
{label}
|
|
44
|
+
{creatable}
|
|
45
|
+
{disabled}
|
|
46
|
+
bind:value
|
|
47
|
+
bind:query
|
|
48
|
+
bind:open
|
|
49
|
+
placeholder="Search frameworks…"
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
39
52
|
<p class="text-xs text-muted">
|
|
40
53
|
Value: <span class="text-primary">{value || '—'}</span>
|
|
41
54
|
</p>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const COMBOBOX_CONTEXT: unique symbol;
|
|
2
|
+
export declare const COMBOBOX_CREATE_KEY = "__create__";
|
|
3
|
+
export interface ComboboxContext {
|
|
4
|
+
getValue: () => string;
|
|
5
|
+
getQuery: () => string;
|
|
6
|
+
getHighlighted: () => string | null;
|
|
7
|
+
select: (value: string) => void;
|
|
8
|
+
highlight: (value: string) => void;
|
|
9
|
+
register: (value: string, disabled: boolean, label?: string) => () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function setComboboxContext(ctx: ComboboxContext): ComboboxContext;
|
|
12
|
+
export declare function getComboboxContext(): ComboboxContext | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
export const COMBOBOX_CONTEXT = Symbol('r2-combobox');
|
|
3
|
+
export const COMBOBOX_CREATE_KEY = '__create__';
|
|
4
|
+
export function setComboboxContext(ctx) {
|
|
5
|
+
setContext(COMBOBOX_CONTEXT, ctx);
|
|
6
|
+
return ctx;
|
|
7
|
+
}
|
|
8
|
+
export function getComboboxContext() {
|
|
9
|
+
return getContext(COMBOBOX_CONTEXT) ?? null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/svelte';
|
|
2
|
+
declare const meta: {
|
|
3
|
+
title: string;
|
|
4
|
+
component: import("svelte").Component<Record<string, never>, {}, "">;
|
|
5
|
+
tags: string[];
|
|
6
|
+
};
|
|
7
|
+
export default meta;
|
|
8
|
+
type Story = StoryObj<typeof meta>;
|
|
9
|
+
export declare const Default: Story;
|