zabi-components 7.0.2 → 8.0.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 +46 -3
- package/THEMING.md +255 -15
- package/dist/atoms/ActionPanel.svelte +12 -7
- package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
- package/dist/atoms/Badge.svelte +93 -56
- package/dist/atoms/Badge.svelte.d.ts +15 -3
- package/dist/atoms/Button.svelte +49 -49
- package/dist/atoms/Button.svelte.d.ts +3 -0
- package/dist/atoms/Card.svelte +31 -28
- package/dist/atoms/Card.svelte.d.ts +5 -2
- package/dist/atoms/CardContent.svelte +10 -2
- package/dist/atoms/CardContent.svelte.d.ts +2 -0
- package/dist/atoms/CardFooter.svelte +10 -2
- package/dist/atoms/CardFooter.svelte.d.ts +2 -0
- package/dist/atoms/CardHeader.svelte +10 -2
- package/dist/atoms/CardHeader.svelte.d.ts +2 -0
- package/dist/atoms/Checkbox.svelte +4 -0
- package/dist/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/atoms/CodeBlock.svelte +15 -7
- package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
- package/dist/atoms/ColorPicker.svelte +19 -9
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
- package/dist/atoms/Container.svelte +2 -3
- package/dist/atoms/Divider.svelte +4 -4
- package/dist/atoms/FeatureCard.svelte +4 -3
- package/dist/atoms/Heading.svelte +56 -27
- package/dist/atoms/Heading.svelte.d.ts +17 -4
- package/dist/atoms/IconButton.svelte +30 -37
- package/dist/atoms/Input.svelte +26 -34
- package/dist/atoms/List.svelte +10 -2
- package/dist/atoms/List.svelte.d.ts +2 -0
- package/dist/atoms/ListItem.svelte +2 -2
- package/dist/atoms/ListItemLeading.svelte +4 -2
- package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
- package/dist/atoms/OptimizedImage.svelte +11 -2
- package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
- package/dist/atoms/Progress.svelte +4 -1
- package/dist/atoms/Progress.svelte.d.ts +2 -0
- package/dist/atoms/Radio.svelte +4 -0
- package/dist/atoms/Radio.svelte.d.ts +2 -0
- package/dist/atoms/Select.svelte +65 -67
- package/dist/atoms/Select.svelte.d.ts +4 -0
- package/dist/atoms/SelectionControl.svelte +5 -1
- package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
- package/dist/atoms/Skeleton.svelte +5 -3
- package/dist/atoms/Table.svelte +2 -1
- package/dist/atoms/Text.svelte +34 -5
- package/dist/atoms/Text.svelte.d.ts +7 -1
- package/dist/atoms/Textarea.svelte +7 -6
- package/dist/atoms/ThemeToggle.svelte +17 -14
- package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
- package/dist/atoms/Toast.svelte +10 -8
- package/dist/atoms/Toggle.svelte +21 -13
- package/dist/atoms/Toggle.svelte.d.ts +7 -0
- package/dist/atoms/Tooltip.svelte +80 -22
- package/dist/atoms/Tooltip.svelte.d.ts +15 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +1 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/util/cn.d.ts +8 -0
- package/dist/components/util/cn.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +4 -4
- package/dist/components/util/ssr-safe.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -6
- package/dist/molecules/Alert.svelte +43 -64
- package/dist/molecules/Alert.svelte.d.ts +7 -1
- package/dist/molecules/ComponentDemo.svelte +10 -2
- package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
- package/dist/molecules/ContactForm.svelte +12 -2
- package/dist/molecules/ContactForm.svelte.d.ts +2 -0
- package/dist/molecules/Dropdown.svelte +40 -11
- package/dist/molecules/Dropdown.svelte.d.ts +4 -0
- package/dist/molecules/EmptyState.svelte +2 -1
- package/dist/molecules/Form.svelte +25 -14
- package/dist/molecules/Form.svelte.d.ts +13 -5
- package/dist/molecules/FormField.svelte +2 -1
- package/dist/molecules/Header.svelte +13 -3
- package/dist/molecules/Header.svelte.d.ts +2 -0
- package/dist/molecules/ImageUpload.svelte +54 -15
- package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
- package/dist/molecules/Modal.svelte +110 -31
- package/dist/molecules/Modal.svelte.d.ts +9 -6
- package/dist/molecules/NavigationMenu.svelte +15 -3
- package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
- package/dist/molecules/NavigationMenuContent.svelte +36 -38
- package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuItem.svelte +10 -2
- package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuLink.svelte +10 -2
- package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuList.svelte +10 -2
- package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
- package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
- package/dist/molecules/Page.svelte +8 -2
- package/dist/molecules/Page.svelte.d.ts +2 -0
- package/dist/molecules/PropsTable.svelte +6 -3
- package/dist/molecules/PropsTable.svelte.d.ts +2 -0
- package/dist/molecules/RadioGroup.svelte +5 -1
- package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
- package/dist/molecules/Section.svelte +12 -4
- package/dist/molecules/Section.svelte.d.ts +3 -1
- package/dist/molecules/SidebarBrandHeader.svelte +13 -4
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
- package/dist/molecules/SidebarFooter.svelte +12 -4
- package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
- package/dist/molecules/SidebarNavSection.svelte +12 -4
- package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
- package/dist/molecules/SlideUp.svelte +85 -21
- package/dist/molecules/SlideUp.svelte.d.ts +5 -4
- package/dist/molecules/Tabs.svelte +25 -19
- package/dist/molecules/Tabs.svelte.d.ts +8 -4
- package/dist/molecules/Toaster.svelte +3 -2
- package/dist/molecules/ToasterToast.svelte +41 -10
- package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
- package/dist/organisms/SidebarAccountPanel.svelte +4 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarNavigation.svelte +102 -59
- package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
- package/dist/organisms/SidebarPanel.svelte +19 -11
- package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarShell.svelte +112 -0
- package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
- package/dist/organisms/TopNavbar.svelte +88 -17
- package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
- package/dist/organisms/index.d.ts +1 -0
- package/dist/organisms/index.js +1 -0
- package/dist/routes/lib/variant-utils.d.ts +26 -0
- package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
- package/dist/types/events.js +1 -0
- package/dist/types/index.js +3 -0
- package/dist/util/cn.d.ts +7 -0
- package/dist/util/cn.js +48 -0
- package/dist/util/ssr-safe.d.ts +4 -4
- package/dist/zabi-components-colors.css +2 -2
- package/dist/zabi-components-theme-dark-only.css +139 -92
- package/dist/zabi-components-theme-dark.css +139 -92
- package/dist/zabi-components-theme-only.css +268 -122
- package/dist/zabi-components-theme.css +268 -122
- package/dist/zabi-components.css +831 -883
- package/docs/lucide-icons.md +3 -1
- package/package.json +19 -24
- package/dist/lib/layout-width-tokens.d.ts +0 -17
- package/dist/lib/layout-width-tokens.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/List.d.ts +0 -3
- package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/index.d.ts +0 -4
- package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
- package/dist/lib/showcase/components-catalog.d.ts +0 -3
- package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
- package/dist/routes/lib/focus-utils.d.ts +0 -12
- package/dist/routes/lib/focus-utils.ts +0 -110
- package/dist/routes/lib/ssr-safe.ts +0 -58
- package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
import Card from "../atoms/Card.svelte";
|
|
4
4
|
import CardHeader from "../atoms/CardHeader.svelte";
|
|
5
5
|
import CardContent from "../atoms/CardContent.svelte";
|
|
6
|
+
import { cn } from "../util/cn.js";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
title: string;
|
|
9
10
|
description?: string;
|
|
10
11
|
code: string;
|
|
11
12
|
language?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** @deprecated use `class`. */
|
|
12
15
|
className?: string;
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -17,11 +20,16 @@
|
|
|
17
20
|
description = "",
|
|
18
21
|
code,
|
|
19
22
|
language = "svelte",
|
|
20
|
-
|
|
23
|
+
class: classAttr = "",
|
|
24
|
+
className: legacyClass = "",
|
|
21
25
|
children,
|
|
22
26
|
...restProps
|
|
23
27
|
} = $props<Props & { children?: any }>();
|
|
24
28
|
|
|
29
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
30
|
+
* Both are merged here so existing call sites keep working. */
|
|
31
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
32
|
+
|
|
25
33
|
let showCode = $state(false);
|
|
26
34
|
</script>
|
|
27
35
|
|
|
@@ -36,7 +44,7 @@
|
|
|
36
44
|
<CardContent>
|
|
37
45
|
<button
|
|
38
46
|
onclick={() => (showCode = !showCode)}
|
|
39
|
-
class="absolute top-8 right-8 z-10 flex cursor-pointer items-center gap-2 rounded-
|
|
47
|
+
class="absolute top-8 right-8 z-10 flex cursor-pointer items-center gap-2 rounded-control px-3 py-2 text-sm text-description transition-colors duration-200 hover:bg-surface-hover hover:text-body"
|
|
40
48
|
aria-label={showCode ? "Show preview" : "Show code"}
|
|
41
49
|
>
|
|
42
50
|
{#if showCode}
|
|
@@ -8,17 +8,25 @@
|
|
|
8
8
|
import CardHeader from "../atoms/CardHeader.svelte";
|
|
9
9
|
import CardContent from "../atoms/CardContent.svelte";
|
|
10
10
|
import type { ContactFormData } from "../types/page.types";
|
|
11
|
+
import { cn } from "../util/cn.js";
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
14
|
+
class?: string;
|
|
15
|
+
/** @deprecated use `class`. */
|
|
13
16
|
className?: string;
|
|
14
17
|
onsubmit?: (event: SubmitEvent) => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
let {
|
|
18
|
-
|
|
21
|
+
class: classAttr = "",
|
|
22
|
+
className: legacyClass = "",
|
|
19
23
|
onsubmit,
|
|
20
24
|
}: Props = $props();
|
|
21
25
|
|
|
26
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
27
|
+
* Both are merged here so existing call sites keep working. */
|
|
28
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
29
|
+
|
|
22
30
|
let formData = $state<ContactFormData>({
|
|
23
31
|
name: "",
|
|
24
32
|
email: "",
|
|
@@ -60,6 +68,8 @@
|
|
|
60
68
|
};
|
|
61
69
|
|
|
62
70
|
if (!validate(data)) {
|
|
71
|
+
// Block native submission so the page does not reload and wipe the errors.
|
|
72
|
+
event.preventDefault();
|
|
63
73
|
formErrorMessage =
|
|
64
74
|
"We could not submit your request. Fix the highlighted fields and try again.";
|
|
65
75
|
return;
|
|
@@ -79,7 +89,7 @@
|
|
|
79
89
|
<Form onsubmit={handleFormSubmit} className="space-y-4">
|
|
80
90
|
{#if formErrorMessage}
|
|
81
91
|
<div
|
|
82
|
-
class="rounded-
|
|
92
|
+
class="rounded-control border border-error px-4 py-3 text-sm text-error"
|
|
83
93
|
role="alert"
|
|
84
94
|
>
|
|
85
95
|
<p class="font-medium">Something went wrong</p>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { generateId } from "../util/ssr-safe.js";
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
4
5
|
|
|
5
6
|
export type DropdownTriggerProps = {
|
|
6
7
|
'aria-expanded': boolean;
|
|
@@ -9,6 +10,8 @@
|
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
13
|
+
/** Extra classes for the host element. */
|
|
14
|
+
class?: string;
|
|
12
15
|
isOpen?: boolean;
|
|
13
16
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
14
17
|
ariaLabel?: string;
|
|
@@ -22,10 +25,13 @@
|
|
|
22
25
|
}>;
|
|
23
26
|
onOptionClick?: (value: string | number) => void;
|
|
24
27
|
trigger: Snippet<[DropdownTriggerProps]>;
|
|
28
|
+
/** Rendered in the popup above the menu/listbox, outside its role (e.g. a search field). */
|
|
29
|
+
header?: Snippet;
|
|
25
30
|
children?: Snippet;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
let {
|
|
34
|
+
class: className = "",
|
|
29
35
|
isOpen = $bindable(false),
|
|
30
36
|
placement = 'bottom-start',
|
|
31
37
|
ariaLabel = 'Menu',
|
|
@@ -34,6 +40,7 @@
|
|
|
34
40
|
options = [],
|
|
35
41
|
onOptionClick,
|
|
36
42
|
trigger,
|
|
43
|
+
header,
|
|
37
44
|
children,
|
|
38
45
|
...restProps
|
|
39
46
|
}: Props = $props();
|
|
@@ -49,8 +56,22 @@
|
|
|
49
56
|
'aria-controls': menuId,
|
|
50
57
|
} satisfies DropdownTriggerProps);
|
|
51
58
|
|
|
59
|
+
function isTextEntryTarget(target: EventTarget | null): boolean {
|
|
60
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
61
|
+
if (target.isContentEditable || target instanceof HTMLTextAreaElement) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
target instanceof HTMLInputElement &&
|
|
66
|
+
!['checkbox', 'radio', 'button', 'submit', 'reset'].includes(target.type)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
function handleKeydown(event: KeyboardEvent) {
|
|
53
71
|
if (!isOpen) {
|
|
72
|
+
if (event.key === ' ' && isTextEntryTarget(event.target)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
54
75
|
if (
|
|
55
76
|
event.key === 'Enter' ||
|
|
56
77
|
event.key === ' ' ||
|
|
@@ -77,10 +98,13 @@
|
|
|
77
98
|
focusPreviousItem();
|
|
78
99
|
break;
|
|
79
100
|
case 'Home':
|
|
101
|
+
// Let text fields (e.g. Select search) keep native caret movement.
|
|
102
|
+
if (isTextEntryTarget(event.target)) break;
|
|
80
103
|
event.preventDefault();
|
|
81
104
|
focusFirstItem();
|
|
82
105
|
break;
|
|
83
106
|
case 'End':
|
|
107
|
+
if (isTextEntryTarget(event.target)) break;
|
|
84
108
|
event.preventDefault();
|
|
85
109
|
focusLastItem();
|
|
86
110
|
break;
|
|
@@ -94,9 +118,13 @@
|
|
|
94
118
|
|
|
95
119
|
function getMenuItems(): HTMLElement[] {
|
|
96
120
|
if (!menuElement) return [];
|
|
121
|
+
// `menuitemradio` and `menuitemcheckbox` are menu items too. Matching
|
|
122
|
+
// only `menuitem` left a menu built from them with nothing to focus:
|
|
123
|
+
// arrows and Home/End did nothing, opening by keyboard focused nothing,
|
|
124
|
+
// and Tab closed the menu, so its items could not be reached at all.
|
|
97
125
|
return Array.from(
|
|
98
126
|
menuElement.querySelectorAll<HTMLElement>(
|
|
99
|
-
'[role="menuitem"], [role="option"]',
|
|
127
|
+
'[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"]',
|
|
100
128
|
),
|
|
101
129
|
);
|
|
102
130
|
}
|
|
@@ -182,7 +210,7 @@
|
|
|
182
210
|
const dropdownContentClasses = $derived(() => {
|
|
183
211
|
return [
|
|
184
212
|
placementClasses(),
|
|
185
|
-
'rounded-
|
|
213
|
+
'rounded-control border border-border bg-surface-overlay py-2 shadow-lg transition-all duration-200 ease-in-out',
|
|
186
214
|
transformClasses(),
|
|
187
215
|
]
|
|
188
216
|
.join(' ')
|
|
@@ -193,12 +221,12 @@
|
|
|
193
221
|
const itemRole = $derived(menuRole === 'listbox' ? 'option' : 'menuitem');
|
|
194
222
|
|
|
195
223
|
const optionClasses =
|
|
196
|
-
'focus-ring flex w-full items-center justify-start rounded-
|
|
224
|
+
'focus-ring flex w-full items-center justify-start rounded-control px-3 py-2 text-left text-sm text-body transition-colors hover:bg-surface-overlay-hover focus:outline-none disabled:cursor-not-allowed disabled:opacity-50';
|
|
197
225
|
</script>
|
|
198
226
|
|
|
199
227
|
<div
|
|
200
228
|
bind:this={rootEl}
|
|
201
|
-
class="relative inline-block"
|
|
229
|
+
class={cn("relative inline-block", className)}
|
|
202
230
|
data-placement={placement}
|
|
203
231
|
onkeydown={handleKeydown}
|
|
204
232
|
{...restProps}
|
|
@@ -206,13 +234,13 @@
|
|
|
206
234
|
{@render trigger(triggerAria)}
|
|
207
235
|
|
|
208
236
|
{#if isOpen}
|
|
209
|
-
<div
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
<div bind:this={menuElement} class={dropdownContentClasses()}>
|
|
238
|
+
{@render header?.()}
|
|
239
|
+
<div
|
|
240
|
+
id={menuId}
|
|
241
|
+
role={menuRole === 'listbox' ? 'listbox' : 'menu'}
|
|
242
|
+
aria-label={ariaLabel}
|
|
243
|
+
>
|
|
216
244
|
{#if options.length > 0}
|
|
217
245
|
<div class="px-2 py-1">
|
|
218
246
|
{#each options as option (option.value)}
|
|
@@ -236,6 +264,7 @@
|
|
|
236
264
|
{:else if children}
|
|
237
265
|
{@render children?.()}
|
|
238
266
|
{/if}
|
|
267
|
+
</div>
|
|
239
268
|
</div>
|
|
240
269
|
{/if}
|
|
241
270
|
</div>
|
|
@@ -5,6 +5,8 @@ export type DropdownTriggerProps = {
|
|
|
5
5
|
'aria-controls': string;
|
|
6
6
|
};
|
|
7
7
|
interface Props {
|
|
8
|
+
/** Extra classes for the host element. */
|
|
9
|
+
class?: string;
|
|
8
10
|
isOpen?: boolean;
|
|
9
11
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
10
12
|
ariaLabel?: string;
|
|
@@ -18,6 +20,8 @@ interface Props {
|
|
|
18
20
|
}>;
|
|
19
21
|
onOptionClick?: (value: string | number) => void;
|
|
20
22
|
trigger: Snippet<[DropdownTriggerProps]>;
|
|
23
|
+
/** Rendered in the popup above the menu/listbox, outside its role (e.g. a search field). */
|
|
24
|
+
header?: Snippet;
|
|
21
25
|
children?: Snippet;
|
|
22
26
|
}
|
|
23
27
|
declare const Dropdown: import("svelte").Component<Props, {}, "isOpen">;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { generateId } from "../util/ssr-safe.js";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
title: string;
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
</script>
|
|
23
24
|
|
|
24
25
|
<section
|
|
25
|
-
class="mx-auto flex flex-col items-center justify-center gap-4 px-6 py-12 text-center
|
|
26
|
+
class={cn("mx-auto flex flex-col items-center justify-center gap-4 px-6 py-12 text-center", className)}
|
|
26
27
|
aria-labelledby={titleId}
|
|
27
28
|
>
|
|
28
29
|
{#if media}
|
|
@@ -1,35 +1,46 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLFormAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
|
|
6
|
+
type Props = Omit<HTMLFormAttributes, "method" | "action" | "onsubmit" | "class"> & {
|
|
3
7
|
method?: "get" | "post";
|
|
4
8
|
action?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
/** @deprecated use `class`. */
|
|
5
11
|
className?: string;
|
|
12
|
+
/** Skip native constraint validation (e.g. when validating in `onsubmit`). */
|
|
13
|
+
novalidate?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Runs on submit. The form still submits natively unless you call
|
|
16
|
+
* `event.preventDefault()` — do so for client-side handling.
|
|
17
|
+
*/
|
|
6
18
|
onsubmit?: (event: SubmitEvent) => void;
|
|
7
|
-
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
};
|
|
8
21
|
|
|
9
22
|
let {
|
|
10
23
|
method = "post",
|
|
11
24
|
action = "",
|
|
12
|
-
|
|
25
|
+
class: classAttr = "",
|
|
26
|
+
className: legacyClass = "",
|
|
27
|
+
novalidate = false,
|
|
13
28
|
onsubmit,
|
|
14
29
|
children,
|
|
15
30
|
...restProps
|
|
16
|
-
} = $props
|
|
17
|
-
|
|
18
|
-
function handleSubmit(event: SubmitEvent) {
|
|
19
|
-
const formData = new FormData(event.target as HTMLFormElement);
|
|
20
|
-
const data = Object.fromEntries(formData.entries());
|
|
31
|
+
}: Props = $props();
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
33
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
34
|
+
* Both are merged here so existing call sites keep working. */
|
|
35
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
26
36
|
</script>
|
|
27
37
|
|
|
28
38
|
<form
|
|
29
39
|
{method}
|
|
30
40
|
{action}
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
novalidate={novalidate || undefined}
|
|
42
|
+
class={cn("space-y-4", className)}
|
|
43
|
+
onsubmit={(event) => onsubmit?.(event)}
|
|
33
44
|
{...restProps}
|
|
34
45
|
>
|
|
35
46
|
{@render children?.()}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLFormAttributes } from "svelte/elements";
|
|
3
|
+
type Props = Omit<HTMLFormAttributes, "method" | "action" | "onsubmit" | "class"> & {
|
|
2
4
|
method?: "get" | "post";
|
|
3
5
|
action?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
/** @deprecated use `class`. */
|
|
4
8
|
className?: string;
|
|
9
|
+
/** Skip native constraint validation (e.g. when validating in `onsubmit`). */
|
|
10
|
+
novalidate?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Runs on submit. The form still submits natively unless you call
|
|
13
|
+
* `event.preventDefault()` — do so for client-side handling.
|
|
14
|
+
*/
|
|
5
15
|
onsubmit?: (event: SubmitEvent) => void;
|
|
6
|
-
|
|
7
|
-
type $$ComponentProps = Props & {
|
|
8
|
-
children?: any;
|
|
16
|
+
children?: Snippet;
|
|
9
17
|
};
|
|
10
|
-
declare const Form: import("svelte").Component
|
|
18
|
+
declare const Form: import("svelte").Component<Props, {}, "">;
|
|
11
19
|
type Form = ReturnType<typeof Form>;
|
|
12
20
|
export default Form;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { generateId } from "../util/ssr-safe.js";
|
|
4
4
|
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
5
6
|
export type FormFieldControlProps = {
|
|
6
7
|
id: string;
|
|
7
8
|
'aria-describedby'?: string;
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
);
|
|
48
49
|
</script>
|
|
49
50
|
|
|
50
|
-
<div class={
|
|
51
|
+
<div class={cn('space-y-2', className)}>
|
|
51
52
|
<div class="flex items-baseline justify-between gap-3">
|
|
52
53
|
<label
|
|
53
54
|
class={[
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
import Badge from "../atoms/Badge.svelte";
|
|
3
3
|
import Heading from "../atoms/Heading.svelte";
|
|
4
4
|
import type { ComponentCategory } from "../types/page.types";
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
8
|
+
/** Extra classes for the host element. */
|
|
9
|
+
class?: string;
|
|
7
10
|
title: string;
|
|
8
11
|
description: string;
|
|
9
12
|
category: ComponentCategory;
|
|
10
13
|
variantsStates?: string[];
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
let {
|
|
16
|
+
let {
|
|
17
|
+
class: className = "",
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
category,
|
|
21
|
+
variantsStates,
|
|
22
|
+
...restProps
|
|
23
|
+
}: Props = $props();
|
|
14
24
|
</script>
|
|
15
25
|
|
|
16
|
-
<header class="space-y-4">
|
|
26
|
+
<header class={cn("space-y-4", className)} {...restProps}>
|
|
17
27
|
<div class="space-y-2">
|
|
18
28
|
<Heading level={1} text={title} />
|
|
19
29
|
<p class="text-lg whitespace-pre-line text-description">{description}</p>
|
|
@@ -32,7 +42,7 @@
|
|
|
32
42
|
Variants / states:
|
|
33
43
|
</span>
|
|
34
44
|
{#each variantsStates as v (v)}
|
|
35
|
-
<span class="rounded-
|
|
45
|
+
<span class="rounded-control bg-base-100 px-2 py-1 text-xs text-headline">
|
|
36
46
|
{v}
|
|
37
47
|
</span>
|
|
38
48
|
{/each}
|
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
4
|
import Button from "../atoms/Button.svelte";
|
|
4
5
|
import { Image } from "@lucide/svelte";
|
|
6
|
+
import { cn } from "../util/cn.js";
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
export type ImageUploadFileDetail = {
|
|
9
|
+
/** Selected file, or `null` when the image is removed. */
|
|
10
|
+
file: File | null;
|
|
11
|
+
/** Object URL used for the preview, or `null`. */
|
|
12
|
+
url: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick"> & {
|
|
16
|
+
/** Extra classes for the host element. */
|
|
17
|
+
class?: string;
|
|
18
|
+
/** Preview URL; `bind:value` to read it. */
|
|
7
19
|
value?: string | null;
|
|
8
20
|
disabled?: boolean;
|
|
9
21
|
accept?: string;
|
|
10
22
|
placeholder?: string;
|
|
11
23
|
errorMessage?: string;
|
|
24
|
+
/** Native `change` event from the hidden file input. */
|
|
12
25
|
onchange?: (event: Event) => void;
|
|
26
|
+
/** Fires when the file chooser is opened. */
|
|
13
27
|
onclick?: (event: Event) => void;
|
|
14
|
-
|
|
28
|
+
/** Selected file and preview URL; `{ file: null, url: null }` on remove. */
|
|
29
|
+
onfileselect?: (detail: ImageUploadFileDetail) => void;
|
|
30
|
+
};
|
|
15
31
|
|
|
16
32
|
let {
|
|
17
|
-
|
|
33
|
+
class: className = "",
|
|
34
|
+
value = $bindable(null),
|
|
18
35
|
disabled = false,
|
|
19
36
|
accept = "image/*",
|
|
20
37
|
placeholder = "No image selected",
|
|
21
38
|
errorMessage = "",
|
|
22
|
-
|
|
39
|
+
onchange,
|
|
40
|
+
onclick,
|
|
41
|
+
onfileselect,
|
|
42
|
+
children: _children,
|
|
23
43
|
...restProps
|
|
24
|
-
} = $props
|
|
44
|
+
}: Props = $props();
|
|
25
45
|
|
|
26
46
|
let fileInput = $state<HTMLInputElement>();
|
|
27
47
|
let currentObjectUrl = $state<string | null>(null);
|
|
@@ -40,13 +60,17 @@
|
|
|
40
60
|
if (!input.files || input.files.length === 0) return;
|
|
41
61
|
|
|
42
62
|
const file = input.files[0];
|
|
63
|
+
let url: string | null = null;
|
|
43
64
|
|
|
44
65
|
if (typeof URL !== "undefined" && URL.createObjectURL) {
|
|
45
66
|
revokeCurrentObjectUrl();
|
|
46
|
-
|
|
67
|
+
url = URL.createObjectURL(file);
|
|
47
68
|
value = url;
|
|
48
69
|
currentObjectUrl = url;
|
|
49
70
|
}
|
|
71
|
+
|
|
72
|
+
onchange?.(event);
|
|
73
|
+
onfileselect?.({ file, url });
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
function removeImage() {
|
|
@@ -54,28 +78,38 @@
|
|
|
54
78
|
|
|
55
79
|
revokeCurrentObjectUrl();
|
|
56
80
|
value = null;
|
|
81
|
+
if (fileInput) fileInput.value = "";
|
|
82
|
+
onfileselect?.({ file: null, url: null });
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
function triggerFileSelect() {
|
|
85
|
+
function triggerFileSelect(event: Event) {
|
|
60
86
|
if (disabled) return;
|
|
87
|
+
onclick?.(event);
|
|
61
88
|
fileInput?.click();
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
function handleDropZoneKeydown(event: KeyboardEvent) {
|
|
92
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
triggerFileSelect(event);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
64
98
|
onDestroy(() => {
|
|
65
99
|
revokeCurrentObjectUrl();
|
|
66
100
|
});
|
|
67
101
|
</script>
|
|
68
102
|
|
|
69
|
-
<div class="space-y-3">
|
|
103
|
+
<div class={cn("space-y-3", className)} {...restProps}>
|
|
70
104
|
{#if value}
|
|
71
105
|
<div class="relative group">
|
|
72
106
|
<img
|
|
73
107
|
src={value}
|
|
74
108
|
alt=""
|
|
75
|
-
class="w-full h-32 min-w-64 object-cover rounded-
|
|
109
|
+
class="w-full h-32 min-w-64 object-cover rounded-container border-0"
|
|
76
110
|
/>
|
|
77
111
|
<div
|
|
78
|
-
class="absolute inset-0 border border-
|
|
112
|
+
class="absolute inset-0 border border-border-overlay bg-surface-overlay/60 backdrop-blur-md min-w-64 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity rounded-container flex items-center justify-center"
|
|
79
113
|
>
|
|
80
114
|
<div class="flex gap-2">
|
|
81
115
|
<Button
|
|
@@ -99,19 +133,18 @@
|
|
|
99
133
|
</div>
|
|
100
134
|
{:else}
|
|
101
135
|
<div
|
|
102
|
-
class="border-2 border-dashed border-input rounded-
|
|
136
|
+
class="border-2 border-dashed border-input-border rounded-container min-w-64 p-6 text-center hover:border-action-primary transition-colors {disabled
|
|
103
137
|
? 'cursor-not-allowed opacity-50'
|
|
104
138
|
: 'cursor-pointer'}"
|
|
105
139
|
onclick={triggerFileSelect}
|
|
106
140
|
role="button"
|
|
107
141
|
tabindex={disabled ? -1 : 0}
|
|
108
|
-
onkeydown={
|
|
109
|
-
e.key === "Enter" && !disabled && triggerFileSelect()}
|
|
142
|
+
onkeydown={handleDropZoneKeydown}
|
|
110
143
|
aria-disabled={disabled}
|
|
111
144
|
>
|
|
112
145
|
<div class="space-y-3">
|
|
113
146
|
<div
|
|
114
|
-
class="w-12 h-12 mx-auto bg-action-secondary rounded-
|
|
147
|
+
class="w-12 h-12 mx-auto bg-action-secondary rounded-control flex items-center justify-center"
|
|
115
148
|
>
|
|
116
149
|
<Image size={24} class="text-description" />
|
|
117
150
|
</div>
|
|
@@ -132,10 +165,16 @@
|
|
|
132
165
|
onchange={handleFileSelect}
|
|
133
166
|
{disabled}
|
|
134
167
|
class="hidden"
|
|
168
|
+
data-testid="image-upload-input"
|
|
135
169
|
/>
|
|
136
170
|
|
|
137
171
|
{#if errorMessage}
|
|
138
|
-
|
|
172
|
+
<!-- Same tinted fill + tinted border Alert uses, rather than a bare
|
|
173
|
+
step-600 outline on a transparent ground. -->
|
|
174
|
+
<div
|
|
175
|
+
class="rounded-container border bg-error-subtle border-error-border text-error-text px-3 py-2 text-sm"
|
|
176
|
+
role="alert"
|
|
177
|
+
>
|
|
139
178
|
<p class="font-medium">Image upload failed</p>
|
|
140
179
|
<p>{errorMessage}</p>
|
|
141
180
|
<p class="mt-1">Recovery action: try another file or retry upload.</p>
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
export type ImageUploadFileDetail = {
|
|
3
|
+
/** Selected file, or `null` when the image is removed. */
|
|
4
|
+
file: File | null;
|
|
5
|
+
/** Object URL used for the preview, or `null`. */
|
|
6
|
+
url: string | null;
|
|
7
|
+
};
|
|
8
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick"> & {
|
|
9
|
+
/** Extra classes for the host element. */
|
|
10
|
+
class?: string;
|
|
11
|
+
/** Preview URL; `bind:value` to read it. */
|
|
2
12
|
value?: string | null;
|
|
3
13
|
disabled?: boolean;
|
|
4
14
|
accept?: string;
|
|
5
15
|
placeholder?: string;
|
|
6
16
|
errorMessage?: string;
|
|
17
|
+
/** Native `change` event from the hidden file input. */
|
|
7
18
|
onchange?: (event: Event) => void;
|
|
19
|
+
/** Fires when the file chooser is opened. */
|
|
8
20
|
onclick?: (event: Event) => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
children?: any;
|
|
21
|
+
/** Selected file and preview URL; `{ file: null, url: null }` on remove. */
|
|
22
|
+
onfileselect?: (detail: ImageUploadFileDetail) => void;
|
|
12
23
|
};
|
|
13
|
-
declare const ImageUpload: import("svelte").Component
|
|
24
|
+
declare const ImageUpload: import("svelte").Component<Props, {}, "value">;
|
|
14
25
|
type ImageUpload = ReturnType<typeof ImageUpload>;
|
|
15
26
|
export default ImageUpload;
|