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.
Files changed (89) hide show
  1. package/README.md +42 -44
  2. package/THEME.md +473 -0
  3. package/THEMING.md +248 -0
  4. package/dist/atoms/Card.svelte +40 -67
  5. package/dist/atoms/Card.svelte.d.ts +3 -3
  6. package/dist/atoms/CardContent.svelte +1 -1
  7. package/dist/atoms/CardHeader.svelte +10 -1
  8. package/dist/atoms/CardHeader.svelte.d.ts +3 -0
  9. package/dist/atoms/Checkbox.svelte +1 -7
  10. package/dist/atoms/CodeBlock.svelte +7 -7
  11. package/dist/atoms/ColorPicker.svelte +81 -73
  12. package/dist/atoms/Input.svelte +16 -12
  13. package/dist/atoms/Input.svelte.d.ts +2 -0
  14. package/dist/atoms/List.svelte +47 -0
  15. package/dist/atoms/List.svelte.d.ts +19 -0
  16. package/dist/atoms/ListItem.svelte +148 -0
  17. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  18. package/dist/atoms/Progress.svelte +4 -6
  19. package/dist/atoms/Select.svelte +65 -20
  20. package/dist/atoms/Select.svelte.d.ts +13 -1
  21. package/dist/atoms/Textarea.svelte +13 -11
  22. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  23. package/dist/atoms/Toast.svelte +1 -1
  24. package/dist/atoms/Toggle.svelte +1 -7
  25. package/dist/atoms/Tooltip.svelte +8 -15
  26. package/dist/atoms/index.d.ts +2 -0
  27. package/dist/atoms/index.js +2 -0
  28. package/dist/components/atoms/index.d.ts +2 -0
  29. package/dist/components/atoms/index.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +4 -1
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/molecules/index.d.ts +0 -1
  33. package/dist/components/molecules/index.d.ts.map +1 -1
  34. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  35. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  36. package/dist/components/organisms/index.d.ts +2 -1
  37. package/dist/components/organisms/index.d.ts.map +1 -1
  38. package/dist/index.d.ts +4 -1
  39. package/dist/index.js +5 -9
  40. package/dist/lib/showcase/components-catalog.d.ts +3 -0
  41. package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
  42. package/dist/lib/showcase/components-showcase-constants.d.ts +63 -0
  43. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
  44. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
  45. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
  46. package/dist/molecules/Alert.svelte +9 -4
  47. package/dist/molecules/ComponentDemo.svelte +53 -50
  48. package/dist/molecules/ContactForm.svelte +113 -76
  49. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  50. package/dist/molecules/Dropdown.svelte +4 -2
  51. package/dist/molecules/ImageUpload.svelte +27 -4
  52. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  53. package/dist/molecules/Modal.svelte +46 -45
  54. package/dist/molecules/NavigationMenu.svelte +7 -10
  55. package/dist/molecules/NavigationMenuContent.svelte +9 -9
  56. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  57. package/dist/molecules/NavigationMenuLink.svelte +8 -6
  58. package/dist/molecules/NavigationMenuList.svelte +0 -7
  59. package/dist/molecules/NavigationMenuTrigger.svelte +8 -11
  60. package/dist/molecules/Section.svelte +12 -12
  61. package/dist/molecules/SlideUp.svelte +32 -2
  62. package/dist/molecules/Tabs.svelte +74 -7
  63. package/dist/molecules/index.d.ts +0 -1
  64. package/dist/molecules/index.js +0 -1
  65. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  66. package/dist/molecules/navigation-menu-context.js +1 -0
  67. package/dist/organisms/Navbar.svelte +170 -53
  68. package/dist/organisms/Navbar.svelte.d.ts +31 -4
  69. package/dist/organisms/SidebarNavigation.svelte +464 -0
  70. package/dist/organisms/SidebarNavigation.svelte.d.ts +49 -0
  71. package/dist/organisms/SidebarPanel.svelte +175 -0
  72. package/dist/organisms/SidebarPanel.svelte.d.ts +32 -0
  73. package/dist/organisms/index.d.ts +2 -1
  74. package/dist/organisms/index.js +2 -1
  75. package/dist/types/page.types.d.ts +32 -0
  76. package/dist/types/page.types.d.ts.map +1 -0
  77. package/dist/types/variants.ts +76 -0
  78. package/dist/zabi-components-colors.css +347 -324
  79. package/dist/zabi-components-theme-dark-only.css +159 -151
  80. package/dist/zabi-components-theme-dark.css +159 -151
  81. package/dist/zabi-components-theme-only.css +245 -230
  82. package/dist/zabi-components-theme.css +245 -230
  83. package/dist/zabi-components.css +722 -84
  84. package/docs/theme-imports.md +112 -0
  85. package/package.json +20 -9
  86. package/dist/molecules/Sidebar.svelte +0 -324
  87. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
  88. package/dist/organisms/Navigation.svelte +0 -116
  89. 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
- variant
72
+ visualVariant
68
73
  ]} transition-all duration-200 motion-reduce:transition-none {className}"
69
74
  role={alertRole}
70
- aria-live={variant === "success" || variant === "info"
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 top-2 right-2 z-10 w-6 h-6 flex items-center justify-center rounded-full text-description hover:text-body transition-colors duration-200 motion-reduce:transition-none group focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2"
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[variant]}
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
- <button
36
- onclick={() => (showCode = !showCode)}
37
- class="absolute top-8 right-8 flex items-center gap-2 px-3 py-1.5 text-sm text-description hover:text-body hover:bg-base-50 rounded-md transition-colors duration-200 z-10"
38
- aria-label={showCode ? "Show preview" : "Show code"}
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
- <svg
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
- <svg
63
- class="w-4 h-4"
64
- fill="none"
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
- </button>
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
- children,
18
- ...restProps
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 formData = new FormData(event.target as HTMLFormElement);
54
+ const submittedFormData = new FormData(event.target as HTMLFormElement);
30
55
  const data: ContactFormData = {
31
- name: (formData.get("name") as string) || "",
32
- email: (formData.get("email") as string) || "",
33
- message: (formData.get("message") as string) || "",
34
- subscribe: formData.get("subscribe") === "on" || false,
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
- console.log("Form submitted with data:", data);
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
- <Card size="md" fullWidth={true} title="Get in Touch">
42
- <Form onsubmit={handleFormSubmit} className="space-y-4">
43
- <div class="space-y-4">
44
- <Input
45
- type="text"
46
- name="name"
47
- label="Name"
48
- placeholder="Enter your name"
49
- value={formData.name}
50
- oninput={(e) =>
51
- (formData.name = (e.target as HTMLInputElement).value)}
52
- />
53
- <Input
54
- type="email"
55
- name="email"
56
- label="Email"
57
- placeholder="Enter your email"
58
- value={formData.email}
59
- oninput={(e) =>
60
- (formData.email = (e.target as HTMLInputElement).value)}
61
- />
62
- <Input
63
- type="email"
64
- name="email"
65
- label="Email"
66
- placeholder="Enter your email"
67
- value={formData.email}
68
- oninput={(e) =>
69
- (formData.email = (e.target as HTMLInputElement).value)}
70
- />
71
- <Input
72
- type="email"
73
- name="email"
74
- label="Email"
75
- placeholder="Enter your email"
76
- value={formData.email}
77
- oninput={(e) =>
78
- (formData.email = (e.target as HTMLInputElement).value)}
79
- />
80
- <Textarea
81
- name="message"
82
- label="Message"
83
- placeholder="Enter your message"
84
- rows={4}
85
- value={formData.message}
86
- oninput={(e) =>
87
- (formData.message = (
88
- e.target as HTMLTextAreaElement
89
- ).value)}
90
- />
91
- <Checkbox
92
- name="subscribe"
93
- label="Subscribe to updates"
94
- checked={formData.subscribe}
95
- onchange={(e) =>
96
- (formData.subscribe = (
97
- e.target as HTMLInputElement
98
- ).checked)}
99
- />
100
- </div>
101
- <div class="pt-4">
102
- <Button type="submit" variant="primary" size="md">
103
- Send Message
104
- </Button>
105
- </div>
106
- </Form>
107
- </Card>
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
- type $$ComponentProps = Props & {
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
- let dropdownId = `dropdown-${Math.random().toString(36).substr(2, 9)}`;
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-0
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
- if (value && typeof URL !== "undefined" && URL.revokeObjectURL) {
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>
@@ -3,6 +3,7 @@ interface Props {
3
3
  disabled?: boolean;
4
4
  accept?: string;
5
5
  placeholder?: string;
6
+ errorMessage?: string;
6
7
  onchange?: (event: Event) => void;
7
8
  onclick?: (event: Event) => void;
8
9
  }
@@ -28,7 +28,7 @@
28
28
  ...restProps
29
29
  }: Props & { children?: any; footer?: any } = $props();
30
30
 
31
- let modalContainer: HTMLDivElement;
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
- if (isOpen && modalContainer) {
56
+ const container = modalContainer;
57
+ if (isOpen && container) {
57
58
  saveFocus();
58
- cleanupFocusTrap = trapFocus(modalContainer);
59
+ cleanupFocusTrap = trapFocus(container);
59
60
  // Small delay to ensure modal is rendered
60
61
  setTimeout(() => {
61
- focusFirstElement(modalContainer);
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 bg-overlay flex items-end md:items-center justify-center p-0 md:p-4 z-modal"
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
- <Card
97
+ <div
97
98
  bind:this={modalContainer}
98
- variant="default"
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
- {#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}
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
- {#if children}
129
- <CardContent className="flex-1">
130
- {@render children?.()}
131
- </CardContent>
132
- {/if}
128
+ {#if children}
129
+ <CardContent className="flex-1">
130
+ {@render children?.()}
131
+ </CardContent>
132
+ {/if}
133
133
 
134
- {#if footer}
135
- <CardFooter className="flex justify-end gap-3 pt-4">
136
- {@render footer?.()}
137
- </CardFooter>
138
- {/if}
139
- </Card>
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}