zabi-components 5.0.21 → 6.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 +42 -44
- package/THEME.md +473 -0
- package/THEMING.md +248 -0
- package/dist/atoms/Card.svelte +40 -67
- package/dist/atoms/Card.svelte.d.ts +3 -3
- package/dist/atoms/CardContent.svelte +1 -1
- package/dist/atoms/CardHeader.svelte +10 -1
- package/dist/atoms/CardHeader.svelte.d.ts +3 -0
- package/dist/atoms/Checkbox.svelte +1 -7
- package/dist/atoms/CodeBlock.svelte +7 -7
- package/dist/atoms/ColorPicker.svelte +81 -73
- package/dist/atoms/Input.svelte +16 -12
- package/dist/atoms/Input.svelte.d.ts +2 -0
- package/dist/atoms/List.svelte +47 -0
- package/dist/atoms/List.svelte.d.ts +19 -0
- package/dist/atoms/ListItem.svelte +148 -0
- package/dist/atoms/ListItem.svelte.d.ts +35 -0
- package/dist/atoms/Progress.svelte +4 -6
- package/dist/atoms/Select.svelte +65 -20
- package/dist/atoms/Select.svelte.d.ts +13 -1
- package/dist/atoms/Textarea.svelte +13 -11
- package/dist/atoms/Textarea.svelte.d.ts +1 -0
- package/dist/atoms/Toast.svelte +1 -1
- package/dist/atoms/Toggle.svelte +1 -7
- package/dist/atoms/Tooltip.svelte +8 -15
- package/dist/atoms/index.d.ts +2 -0
- package/dist/atoms/index.js +2 -0
- package/dist/components/atoms/index.d.ts +2 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +0 -1
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
- package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
- package/dist/components/organisms/index.d.ts +2 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -9
- package/dist/lib/showcase/components-catalog.d.ts +3 -0
- package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts +63 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
- package/dist/molecules/Alert.svelte +9 -4
- package/dist/molecules/ComponentDemo.svelte +53 -50
- package/dist/molecules/ContactForm.svelte +113 -76
- package/dist/molecules/ContactForm.svelte.d.ts +1 -4
- package/dist/molecules/Dropdown.svelte +4 -2
- package/dist/molecules/ImageUpload.svelte +27 -4
- package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
- package/dist/molecules/Modal.svelte +46 -45
- package/dist/molecules/NavigationMenu.svelte +7 -10
- package/dist/molecules/NavigationMenuContent.svelte +9 -9
- package/dist/molecules/NavigationMenuItem.svelte +7 -6
- package/dist/molecules/NavigationMenuLink.svelte +8 -6
- package/dist/molecules/NavigationMenuList.svelte +0 -7
- package/dist/molecules/NavigationMenuTrigger.svelte +8 -11
- package/dist/molecules/Section.svelte +12 -12
- package/dist/molecules/SlideUp.svelte +32 -2
- package/dist/molecules/Tabs.svelte +74 -7
- package/dist/molecules/index.d.ts +0 -1
- package/dist/molecules/index.js +0 -1
- package/dist/molecules/navigation-menu-context.d.ts +6 -0
- package/dist/molecules/navigation-menu-context.js +1 -0
- package/dist/organisms/Navbar.svelte +170 -53
- package/dist/organisms/Navbar.svelte.d.ts +31 -4
- package/dist/organisms/SidebarNavigation.svelte +464 -0
- package/dist/organisms/SidebarNavigation.svelte.d.ts +49 -0
- package/dist/organisms/SidebarPanel.svelte +175 -0
- package/dist/organisms/SidebarPanel.svelte.d.ts +32 -0
- package/dist/organisms/index.d.ts +2 -1
- package/dist/organisms/index.js +2 -1
- package/dist/types/page.types.d.ts +32 -0
- package/dist/types/page.types.d.ts.map +1 -0
- package/dist/types/variants.ts +76 -0
- package/dist/zabi-components-colors.css +347 -324
- package/dist/zabi-components-theme-dark-only.css +159 -151
- package/dist/zabi-components-theme-dark.css +159 -151
- package/dist/zabi-components-theme-only.css +245 -230
- package/dist/zabi-components-theme.css +245 -230
- package/dist/zabi-components.css +722 -84
- package/docs/theme-imports.md +112 -0
- package/package.json +20 -9
- package/dist/molecules/Sidebar.svelte +0 -324
- package/dist/molecules/Sidebar.svelte.d.ts +0 -30
- package/dist/organisms/Navigation.svelte +0 -116
- package/dist/organisms/Navigation.svelte.d.ts +0 -20
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ExtendedSemanticVariant } from "../../types/variants.js";
|
|
3
|
+
type AlertVisualVariant = Exclude<ExtendedSemanticVariant, "default">;
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
variant?: ExtendedSemanticVariant;
|
|
@@ -27,6 +28,10 @@
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const visualVariant = $derived<AlertVisualVariant>(
|
|
32
|
+
variant === "default" ? "info" : variant,
|
|
33
|
+
);
|
|
34
|
+
|
|
30
35
|
let alertClasses = $derived({
|
|
31
36
|
info: "text-info border border-info",
|
|
32
37
|
success: "text-success border border-success",
|
|
@@ -64,10 +69,10 @@
|
|
|
64
69
|
|
|
65
70
|
<div
|
|
66
71
|
class="relative rounded-md p-4 border {alertClasses[
|
|
67
|
-
|
|
72
|
+
visualVariant
|
|
68
73
|
]} transition-all duration-200 motion-reduce:transition-none {className}"
|
|
69
74
|
role={alertRole}
|
|
70
|
-
aria-live={
|
|
75
|
+
aria-live={visualVariant === "success" || visualVariant === "info"
|
|
71
76
|
? "polite"
|
|
72
77
|
: "assertive"}
|
|
73
78
|
aria-atomic="true"
|
|
@@ -76,7 +81,7 @@
|
|
|
76
81
|
{#if closable}
|
|
77
82
|
<button
|
|
78
83
|
onclick={handleDismiss}
|
|
79
|
-
class="absolute
|
|
84
|
+
class="group absolute right-2 top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-description transition-colors duration-200 hover:text-body motion-reduce:transition-none focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2"
|
|
80
85
|
aria-label="Dismiss alert"
|
|
81
86
|
type="button"
|
|
82
87
|
>
|
|
@@ -98,7 +103,7 @@
|
|
|
98
103
|
|
|
99
104
|
<div class="flex items-start gap-3 {closable ? 'pr-8' : ''}">
|
|
100
105
|
<div class="shrink-0 mt-0.5">
|
|
101
|
-
{@html iconSvg[
|
|
106
|
+
{@html iconSvg[visualVariant]}
|
|
102
107
|
</div>
|
|
103
108
|
|
|
104
109
|
<div class="flex-1 min-w-0">
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CodeBlock from "../atoms/CodeBlock.svelte";
|
|
3
3
|
import Card from "../atoms/Card.svelte";
|
|
4
|
+
import CardHeader from "../atoms/CardHeader.svelte";
|
|
5
|
+
import CardContent from "../atoms/CardContent.svelte";
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
title: string;
|
|
@@ -27,61 +29,62 @@
|
|
|
27
29
|
<Card
|
|
28
30
|
size="lg"
|
|
29
31
|
fullWidth={true}
|
|
30
|
-
{title}
|
|
31
|
-
{description}
|
|
32
32
|
className="overflow-hidden {className}"
|
|
33
33
|
{...restProps}
|
|
34
34
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
<CardHeader {title} {description} />
|
|
36
|
+
<CardContent>
|
|
37
|
+
<button
|
|
38
|
+
onclick={() => (showCode = !showCode)}
|
|
39
|
+
class="absolute top-8 right-8 z-10 flex cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 text-sm text-description transition-colors duration-200 hover:bg-base-50 hover:text-body"
|
|
40
|
+
aria-label={showCode ? "Show preview" : "Show code"}
|
|
41
|
+
>
|
|
42
|
+
{#if showCode}
|
|
43
|
+
<svg
|
|
44
|
+
class="w-4 h-4"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
viewBox="0 0 24 24"
|
|
48
|
+
>
|
|
49
|
+
<path
|
|
50
|
+
stroke-linecap="round"
|
|
51
|
+
stroke-linejoin="round"
|
|
52
|
+
stroke-width="2"
|
|
53
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
54
|
+
/>
|
|
55
|
+
<path
|
|
56
|
+
stroke-linecap="round"
|
|
57
|
+
stroke-linejoin="round"
|
|
58
|
+
stroke-width="2"
|
|
59
|
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
Preview
|
|
63
|
+
{:else}
|
|
64
|
+
<svg
|
|
65
|
+
class="w-4 h-4"
|
|
66
|
+
fill="none"
|
|
67
|
+
stroke="currentColor"
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
>
|
|
70
|
+
<path
|
|
71
|
+
stroke-linecap="round"
|
|
72
|
+
stroke-linejoin="round"
|
|
73
|
+
stroke-width="2"
|
|
74
|
+
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
|
75
|
+
/>
|
|
76
|
+
</svg>
|
|
77
|
+
Code
|
|
78
|
+
{/if}
|
|
79
|
+
</button>
|
|
80
|
+
|
|
40
81
|
{#if showCode}
|
|
41
|
-
<
|
|
42
|
-
class="w-4 h-4"
|
|
43
|
-
fill="none"
|
|
44
|
-
stroke="currentColor"
|
|
45
|
-
viewBox="0 0 24 24"
|
|
46
|
-
>
|
|
47
|
-
<path
|
|
48
|
-
stroke-linecap="round"
|
|
49
|
-
stroke-linejoin="round"
|
|
50
|
-
stroke-width="2"
|
|
51
|
-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
52
|
-
/>
|
|
53
|
-
<path
|
|
54
|
-
stroke-linecap="round"
|
|
55
|
-
stroke-linejoin="round"
|
|
56
|
-
stroke-width="2"
|
|
57
|
-
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
58
|
-
/>
|
|
59
|
-
</svg>
|
|
60
|
-
Preview
|
|
82
|
+
<CodeBlock {code} {language} />
|
|
61
83
|
{:else}
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
stroke="currentColor"
|
|
66
|
-
viewBox="0 0 24 24"
|
|
67
|
-
>
|
|
68
|
-
<path
|
|
69
|
-
stroke-linecap="round"
|
|
70
|
-
stroke-linejoin="round"
|
|
71
|
-
stroke-width="2"
|
|
72
|
-
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
|
73
|
-
/>
|
|
74
|
-
</svg>
|
|
75
|
-
Code
|
|
84
|
+
<div class="min-h-[100px] flex items-center justify-center w-full">
|
|
85
|
+
{@render children?.()}
|
|
86
|
+
</div>
|
|
76
87
|
{/if}
|
|
77
|
-
</
|
|
78
|
-
|
|
79
|
-
{#if showCode}
|
|
80
|
-
<CodeBlock {code} {language} />
|
|
81
|
-
{:else}
|
|
82
|
-
<div class="min-h-[100px] flex items-center justify-center w-full">
|
|
83
|
-
{@render children?.()}
|
|
84
|
-
</div>
|
|
85
|
-
{/if}
|
|
88
|
+
</CardContent>
|
|
86
89
|
</Card>
|
|
87
90
|
</div>
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import Checkbox from "../atoms/Checkbox.svelte";
|
|
6
6
|
import Button from "../atoms/Button.svelte";
|
|
7
7
|
import Card from "../atoms/Card.svelte";
|
|
8
|
+
import CardHeader from "../atoms/CardHeader.svelte";
|
|
9
|
+
import CardContent from "../atoms/CardContent.svelte";
|
|
8
10
|
import type { ContactFormData } from "../../types/page.types";
|
|
9
11
|
|
|
10
12
|
interface Props {
|
|
@@ -14,9 +16,8 @@
|
|
|
14
16
|
|
|
15
17
|
let {
|
|
16
18
|
className = "",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} = $props<Props & { children?: any }>();
|
|
19
|
+
onsubmit,
|
|
20
|
+
}: Props = $props();
|
|
20
21
|
|
|
21
22
|
let formData = $state<ContactFormData>({
|
|
22
23
|
name: "",
|
|
@@ -25,83 +26,119 @@
|
|
|
25
26
|
subscribe: false,
|
|
26
27
|
});
|
|
27
28
|
|
|
29
|
+
let fieldErrors = $state<Partial<Record<keyof ContactFormData, string>>>({});
|
|
30
|
+
let formErrorMessage = $state("");
|
|
31
|
+
|
|
32
|
+
function validate(data: ContactFormData) {
|
|
33
|
+
const nextErrors: Partial<Record<keyof ContactFormData, string>> = {};
|
|
34
|
+
|
|
35
|
+
if (!data.name.trim()) {
|
|
36
|
+
nextErrors.name = "Please enter your name.";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!data.email.trim()) {
|
|
40
|
+
nextErrors.email = "Please enter your email address.";
|
|
41
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
42
|
+
nextErrors.email = "Please enter a valid email address.";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!(data.message || "").trim()) {
|
|
46
|
+
nextErrors.message = "Please enter a message.";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fieldErrors = nextErrors;
|
|
50
|
+
return Object.keys(nextErrors).length === 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
function handleFormSubmit(event: SubmitEvent) {
|
|
29
|
-
const
|
|
54
|
+
const submittedFormData = new FormData(event.target as HTMLFormElement);
|
|
30
55
|
const data: ContactFormData = {
|
|
31
|
-
name: (
|
|
32
|
-
email: (
|
|
33
|
-
message: (
|
|
34
|
-
subscribe:
|
|
56
|
+
name: (submittedFormData.get("name") as string) || "",
|
|
57
|
+
email: (submittedFormData.get("email") as string) || "",
|
|
58
|
+
message: (submittedFormData.get("message") as string) || "",
|
|
59
|
+
subscribe: submittedFormData.get("subscribe") === "on" || false,
|
|
35
60
|
};
|
|
36
61
|
|
|
37
|
-
|
|
62
|
+
if (!validate(data)) {
|
|
63
|
+
formErrorMessage =
|
|
64
|
+
"We could not submit your request. Fix the highlighted fields and try again.";
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
formErrorMessage = "";
|
|
69
|
+
if (onsubmit) {
|
|
70
|
+
onsubmit(event);
|
|
71
|
+
}
|
|
38
72
|
}
|
|
39
73
|
</script>
|
|
40
74
|
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
</
|
|
75
|
+
<div class={className}>
|
|
76
|
+
<Card size="md" fullWidth={true}>
|
|
77
|
+
<CardHeader title="Get in Touch" />
|
|
78
|
+
<CardContent>
|
|
79
|
+
<Form onsubmit={handleFormSubmit} className="space-y-4">
|
|
80
|
+
{#if formErrorMessage}
|
|
81
|
+
<div
|
|
82
|
+
class="rounded-lg border border-error px-4 py-3 text-sm text-error"
|
|
83
|
+
role="alert"
|
|
84
|
+
>
|
|
85
|
+
<p class="font-medium">Something went wrong</p>
|
|
86
|
+
<p>{formErrorMessage}</p>
|
|
87
|
+
<p class="mt-1">Recovery action: review your inputs and resubmit.</p>
|
|
88
|
+
</div>
|
|
89
|
+
{/if}
|
|
90
|
+
<div class="space-y-4">
|
|
91
|
+
<Input
|
|
92
|
+
type="text"
|
|
93
|
+
name="name"
|
|
94
|
+
label="Name"
|
|
95
|
+
placeholder="Enter your name"
|
|
96
|
+
value={formData.name}
|
|
97
|
+
oninput={(e) =>
|
|
98
|
+
(formData.name = (e.target as HTMLInputElement).value)}
|
|
99
|
+
variant={fieldErrors.name ? "error" : "default"}
|
|
100
|
+
message={fieldErrors.name || ""}
|
|
101
|
+
/>
|
|
102
|
+
<Input
|
|
103
|
+
type="email"
|
|
104
|
+
name="email"
|
|
105
|
+
label="Email"
|
|
106
|
+
placeholder="Enter your email"
|
|
107
|
+
value={formData.email}
|
|
108
|
+
oninput={(e) =>
|
|
109
|
+
(formData.email = (e.target as HTMLInputElement).value)}
|
|
110
|
+
variant={fieldErrors.email ? "error" : "default"}
|
|
111
|
+
message={fieldErrors.email || ""}
|
|
112
|
+
/>
|
|
113
|
+
<Textarea
|
|
114
|
+
name="message"
|
|
115
|
+
label="Message"
|
|
116
|
+
placeholder="Enter your message"
|
|
117
|
+
rows={4}
|
|
118
|
+
value={formData.message}
|
|
119
|
+
oninput={(e) =>
|
|
120
|
+
(formData.message = (
|
|
121
|
+
e.target as HTMLTextAreaElement
|
|
122
|
+
).value)}
|
|
123
|
+
variant={fieldErrors.message ? "error" : "default"}
|
|
124
|
+
message={fieldErrors.message || ""}
|
|
125
|
+
/>
|
|
126
|
+
<Checkbox
|
|
127
|
+
name="subscribe"
|
|
128
|
+
label="Subscribe to updates"
|
|
129
|
+
checked={formData.subscribe}
|
|
130
|
+
onchange={(e) =>
|
|
131
|
+
(formData.subscribe = (
|
|
132
|
+
e.target as HTMLInputElement
|
|
133
|
+
).checked)}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="pt-4">
|
|
137
|
+
<Button type="submit" variant="primary" size="md">
|
|
138
|
+
Send Message
|
|
139
|
+
</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</Form>
|
|
142
|
+
</CardContent>
|
|
143
|
+
</Card>
|
|
144
|
+
</div>
|
|
@@ -2,9 +2,6 @@ interface Props {
|
|
|
2
2
|
className?: string;
|
|
3
3
|
onsubmit?: (event: SubmitEvent) => void;
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
children?: any;
|
|
7
|
-
};
|
|
8
|
-
declare const ContactForm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
5
|
+
declare const ContactForm: import("svelte").Component<Props, {}, "">;
|
|
9
6
|
type ContactForm = ReturnType<typeof ContactForm>;
|
|
10
7
|
export default ContactForm;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Button from "../atoms/Button.svelte";
|
|
3
|
+
import { generateId } from "../../routes/lib/ssr-safe.js";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
isOpen?: boolean;
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
...restProps
|
|
27
28
|
}: Props & { children?: any; trigger?: any } = $props();
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
const dropdownId = generateId("dropdown");
|
|
30
31
|
let triggerElement = $state<HTMLElement | null>(null);
|
|
31
32
|
let menuElement = $state<HTMLElement | null>(null);
|
|
32
33
|
let openedViaKeyboard = $state(false);
|
|
@@ -181,7 +182,8 @@
|
|
|
181
182
|
bg-input
|
|
182
183
|
rounded-lg
|
|
183
184
|
shadow-lg
|
|
184
|
-
border
|
|
185
|
+
border
|
|
186
|
+
border-input-border
|
|
185
187
|
py-2
|
|
186
188
|
transition-all
|
|
187
189
|
duration-200
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from "svelte";
|
|
2
3
|
import Button from "../atoms/Button.svelte";
|
|
3
4
|
import { Image } from "@lucide/svelte";
|
|
4
5
|
|
|
@@ -7,6 +8,7 @@
|
|
|
7
8
|
disabled?: boolean;
|
|
8
9
|
accept?: string;
|
|
9
10
|
placeholder?: string;
|
|
11
|
+
errorMessage?: string;
|
|
10
12
|
onchange?: (event: Event) => void;
|
|
11
13
|
onclick?: (event: Event) => void;
|
|
12
14
|
}
|
|
@@ -16,11 +18,20 @@
|
|
|
16
18
|
disabled = false,
|
|
17
19
|
accept = "image/*",
|
|
18
20
|
placeholder = "No image selected",
|
|
21
|
+
errorMessage = "",
|
|
19
22
|
children,
|
|
20
23
|
...restProps
|
|
21
24
|
} = $props<Props & { children?: any }>();
|
|
22
25
|
|
|
23
26
|
let fileInput = $state<HTMLInputElement>();
|
|
27
|
+
let currentObjectUrl = $state<string | null>(null);
|
|
28
|
+
|
|
29
|
+
function revokeCurrentObjectUrl() {
|
|
30
|
+
if (currentObjectUrl && typeof URL !== "undefined" && URL.revokeObjectURL) {
|
|
31
|
+
URL.revokeObjectURL(currentObjectUrl);
|
|
32
|
+
currentObjectUrl = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
function handleFileSelect(event: Event) {
|
|
26
37
|
if (disabled) return;
|
|
@@ -31,17 +42,17 @@
|
|
|
31
42
|
const file = input.files[0];
|
|
32
43
|
|
|
33
44
|
if (typeof URL !== "undefined" && URL.createObjectURL) {
|
|
45
|
+
revokeCurrentObjectUrl();
|
|
34
46
|
const url = URL.createObjectURL(file);
|
|
35
47
|
value = url;
|
|
48
|
+
currentObjectUrl = url;
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
function removeImage() {
|
|
40
53
|
if (disabled) return;
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
URL.revokeObjectURL(value);
|
|
44
|
-
}
|
|
55
|
+
revokeCurrentObjectUrl();
|
|
45
56
|
value = null;
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -49,6 +60,10 @@
|
|
|
49
60
|
if (disabled) return;
|
|
50
61
|
fileInput?.click();
|
|
51
62
|
}
|
|
63
|
+
|
|
64
|
+
onDestroy(() => {
|
|
65
|
+
revokeCurrentObjectUrl();
|
|
66
|
+
});
|
|
52
67
|
</script>
|
|
53
68
|
|
|
54
69
|
<div class="space-y-3">
|
|
@@ -60,7 +75,7 @@
|
|
|
60
75
|
class="w-full h-32 min-w-64 object-cover rounded-2xl border-0"
|
|
61
76
|
/>
|
|
62
77
|
<div
|
|
63
|
-
class="absolute inset-0 bg-input min-w-64 opacity-0 group-hover:opacity-100 transition-opacity rounded-2xl flex items-center justify-center"
|
|
78
|
+
class="absolute inset-0 border border-input-border bg-input min-w-64 opacity-0 group-hover:opacity-100 transition-opacity rounded-2xl flex items-center justify-center"
|
|
64
79
|
>
|
|
65
80
|
<div class="flex gap-2">
|
|
66
81
|
<Button
|
|
@@ -118,4 +133,12 @@
|
|
|
118
133
|
{disabled}
|
|
119
134
|
class="hidden"
|
|
120
135
|
/>
|
|
136
|
+
|
|
137
|
+
{#if errorMessage}
|
|
138
|
+
<div class="rounded-lg border border-error px-3 py-2 text-sm text-error" role="alert">
|
|
139
|
+
<p class="font-medium">Image upload failed</p>
|
|
140
|
+
<p>{errorMessage}</p>
|
|
141
|
+
<p class="mt-1">Recovery action: try another file or retry upload.</p>
|
|
142
|
+
</div>
|
|
143
|
+
{/if}
|
|
121
144
|
</div>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
...restProps
|
|
29
29
|
}: Props & { children?: any; footer?: any } = $props();
|
|
30
30
|
|
|
31
|
-
let modalContainer
|
|
31
|
+
let modalContainer = $state<HTMLDivElement>();
|
|
32
32
|
let cleanupFocusTrap: (() => void) | null = null;
|
|
33
33
|
|
|
34
34
|
const sizeClasses = $derived(
|
|
@@ -53,12 +53,13 @@
|
|
|
53
53
|
|
|
54
54
|
// Handle focus trap when modal opens
|
|
55
55
|
$effect(() => {
|
|
56
|
-
|
|
56
|
+
const container = modalContainer;
|
|
57
|
+
if (isOpen && container) {
|
|
57
58
|
saveFocus();
|
|
58
|
-
cleanupFocusTrap = trapFocus(
|
|
59
|
+
cleanupFocusTrap = trapFocus(container);
|
|
59
60
|
// Small delay to ensure modal is rendered
|
|
60
61
|
setTimeout(() => {
|
|
61
|
-
focusFirstElement(
|
|
62
|
+
focusFirstElement(container);
|
|
62
63
|
}, 0);
|
|
63
64
|
} else if (!isOpen && cleanupFocusTrap) {
|
|
64
65
|
cleanupFocusTrap();
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
|
|
86
87
|
{#if isOpen}
|
|
87
88
|
<div
|
|
88
|
-
class="fixed inset-0
|
|
89
|
+
class="fixed inset-0 z-modal flex cursor-pointer items-end justify-center bg-overlay p-0 md:items-center md:p-4"
|
|
89
90
|
onclick={handleBackdropClick}
|
|
90
91
|
onkeydown={handleKeydown}
|
|
91
92
|
role="dialog"
|
|
@@ -93,49 +94,49 @@
|
|
|
93
94
|
aria-labelledby={title ? "modal-title" : undefined}
|
|
94
95
|
tabindex="-1"
|
|
95
96
|
>
|
|
96
|
-
<
|
|
97
|
+
<div
|
|
97
98
|
bind:this={modalContainer}
|
|
98
|
-
|
|
99
|
-
fullWidth={false}
|
|
100
|
-
class="bg-card rounded-t-3xl md:rounded-3xl shadow-xl min-w-[320px] {sizeClasses} max-h-[90vh] overflow-y-auto animate-[slideUp_0.3s_ease-out] md:animate-none flex flex-col p-0"
|
|
99
|
+
class="flex max-h-[90vh] min-w-[320px] cursor-default flex-col overflow-y-auto rounded-t-3xl bg-card p-0 shadow-xl animate-[slideUp_0.3s_ease-out] md:animate-none md:rounded-3xl {sizeClasses}"
|
|
101
100
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
<Card variant="default" fullWidth={false}>
|
|
102
|
+
{#if title || description}
|
|
103
|
+
<CardHeader
|
|
104
|
+
{description}
|
|
105
|
+
className="px-6 pt-6 pb-4"
|
|
106
|
+
>
|
|
107
|
+
{#if title}
|
|
108
|
+
<div class="flex items-center justify-between">
|
|
109
|
+
<h2
|
|
110
|
+
id="modal-title"
|
|
111
|
+
class="text-2xl font-normal leading-8 text-headline tracking-normal"
|
|
112
|
+
>
|
|
113
|
+
{title}
|
|
114
|
+
</h2>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onclick={closeModal}
|
|
118
|
+
class="text-description hover:text-headline text-2xl cursor-pointer transition-colors w-8 h-8 flex items-center justify-center rounded-full hover:bg-base-100"
|
|
119
|
+
aria-label="Close"
|
|
120
|
+
>
|
|
121
|
+
×
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
</CardHeader>
|
|
126
|
+
{/if}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
{#if children}
|
|
129
|
+
<CardContent className="flex-1">
|
|
130
|
+
{@render children?.()}
|
|
131
|
+
</CardContent>
|
|
132
|
+
{/if}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
{#if footer}
|
|
135
|
+
<CardFooter className="flex justify-end gap-3 pt-4">
|
|
136
|
+
{@render footer?.()}
|
|
137
|
+
</CardFooter>
|
|
138
|
+
{/if}
|
|
139
|
+
</Card>
|
|
140
|
+
</div>
|
|
140
141
|
</div>
|
|
141
142
|
{/if}
|