zabi-components 5.0.19 → 5.0.21

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +354 -81
  3. package/dist/atoms/Button.svelte +6 -3
  4. package/dist/atoms/Button.svelte.d.ts +1 -0
  5. package/dist/atoms/Card.svelte +23 -47
  6. package/dist/atoms/CardContent.svelte +12 -5
  7. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  8. package/dist/atoms/CardHeader.svelte +22 -5
  9. package/dist/atoms/CardHeader.svelte.d.ts +3 -0
  10. package/dist/atoms/ColorPicker.svelte +271 -7
  11. package/dist/atoms/IconButton.svelte +88 -0
  12. package/dist/atoms/IconButton.svelte.d.ts +21 -0
  13. package/dist/atoms/Input.svelte +7 -6
  14. package/dist/atoms/Input.svelte.d.ts +3 -1
  15. package/dist/atoms/Progress.svelte +1 -3
  16. package/dist/atoms/Select.svelte +109 -24
  17. package/dist/atoms/Select.svelte.d.ts +10 -0
  18. package/dist/atoms/Skeleton.svelte +12 -2
  19. package/dist/atoms/Skeleton.svelte.d.ts +1 -0
  20. package/dist/atoms/Tooltip.svelte +15 -17
  21. package/dist/atoms/index.d.ts +1 -2
  22. package/dist/atoms/index.js +1 -2
  23. package/dist/components/atoms/index.d.ts +1 -2
  24. package/dist/components/atoms/index.d.ts.map +1 -1
  25. package/dist/components/molecules/index.d.ts +8 -0
  26. package/dist/components/molecules/index.d.ts.map +1 -1
  27. package/dist/molecules/Dropdown.svelte +101 -14
  28. package/dist/molecules/Dropdown.svelte.d.ts +7 -0
  29. package/dist/molecules/ImageUpload.svelte +4 -4
  30. package/dist/molecules/Modal.svelte +42 -26
  31. package/dist/molecules/Modal.svelte.d.ts +1 -0
  32. package/dist/molecules/NavigationMenu.svelte +158 -0
  33. package/dist/molecules/NavigationMenu.svelte.d.ts +25 -0
  34. package/dist/molecules/NavigationMenuContent.svelte +83 -0
  35. package/dist/molecules/NavigationMenuContent.svelte.d.ts +9 -0
  36. package/dist/molecules/NavigationMenuItem.svelte +36 -0
  37. package/dist/molecules/NavigationMenuItem.svelte.d.ts +9 -0
  38. package/dist/molecules/NavigationMenuLink.svelte +53 -0
  39. package/dist/molecules/NavigationMenuLink.svelte.d.ts +10 -0
  40. package/dist/molecules/NavigationMenuList.svelte +31 -0
  41. package/dist/molecules/NavigationMenuList.svelte.d.ts +8 -0
  42. package/dist/molecules/NavigationMenuTrigger.svelte +81 -0
  43. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +9 -0
  44. package/dist/molecules/Section.svelte +158 -0
  45. package/dist/molecules/Section.svelte.d.ts +27 -0
  46. package/dist/molecules/Sidebar.svelte +324 -0
  47. package/dist/molecules/Sidebar.svelte.d.ts +30 -0
  48. package/dist/molecules/SlideUp.svelte +2 -2
  49. package/dist/molecules/index.d.ts +8 -0
  50. package/dist/molecules/index.js +8 -0
  51. package/dist/organisms/Navigation.svelte +2 -2
  52. package/dist/zabi-components-colors.css +2 -2
  53. package/dist/zabi-components-theme-only.css +2 -2
  54. package/dist/zabi-components-theme.css +2 -2
  55. package/dist/zabi-components.css +299 -24
  56. package/package.json +3 -5
  57. package/dist/atoms/CardDescription.svelte +0 -24
  58. package/dist/atoms/CardDescription.svelte.d.ts +0 -8
  59. package/dist/atoms/CardTitle.svelte +0 -33
  60. package/dist/atoms/CardTitle.svelte.d.ts +0 -9
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import type { CardVariant, SizeVariant } from "../../types/variants.js";
4
+ import CardHeader from "./CardHeader.svelte";
5
+ import CardContent from "./CardContent.svelte";
4
6
 
5
7
  interface Props {
6
8
  title?: string;
@@ -42,11 +44,12 @@
42
44
  }
43
45
  });
44
46
 
45
- const useOldAPI = $derived(title !== "" || description !== "" || image !== "");
46
-
47
+ const useOldAPI = $derived(
48
+ title !== "" || description !== "" || image !== "",
49
+ );
50
+
47
51
  const cardClasses = $derived(() => {
48
- const baseClasses =
49
- `rounded-lg transition-all duration-200 ${variantClasses()}`;
52
+ const baseClasses = `rounded-lg transition-all duration-200 ${variantClasses()}`;
50
53
  const interactiveClasses = onclick
51
54
  ? variant === "elevated"
52
55
  ? "cursor-pointer hover:shadow-xl hover:bg-card-hover"
@@ -63,26 +66,6 @@
63
66
  return `${baseClasses} ${interactiveClasses} ${widthClasses} ${minWidthClass} ${paddingClass}`.trim();
64
67
  });
65
68
 
66
- const titleClasses = $derived(() => {
67
- if (size === "sm") {
68
- return "text-lg font-medium mb-2 text-headline";
69
- } else if (size === "lg") {
70
- return "text-2xl font-medium mb-4 text-headline";
71
- } else {
72
- return "text-xl font-medium mb-3 text-headline";
73
- }
74
- });
75
-
76
- const descriptionClasses = $derived(() => {
77
- if (size === "sm") {
78
- return "text-xs text-description mb-3";
79
- } else if (size === "lg") {
80
- return "text-base text-description mb-5";
81
- } else {
82
- return "text-sm text-description mb-4";
83
- }
84
- });
85
-
86
69
  function handleKeydown(event: KeyboardEvent) {
87
70
  if (onclick && (event.key === "Enter" || event.key === " ")) {
88
71
  event.preventDefault();
@@ -91,44 +74,37 @@
91
74
  }
92
75
 
93
76
  const cardRole = $derived(onclick ? "button" : undefined);
94
- const cardTabIndex = $derived(onclick ? 0 : undefined);
95
77
  const cardAriaLabel = $derived(onclick && title ? title : undefined);
96
78
  </script>
97
79
 
98
- <div
99
- class={cardClasses()}
100
- {onclick}
80
+ <div
81
+ class={cardClasses()}
82
+ {onclick}
101
83
  role={cardRole}
102
- tabindex={cardTabIndex}
84
+ {...onclick ? { tabindex: 0 } : {}}
103
85
  aria-label={cardAriaLabel}
104
- onkeydown={handleKeydown}
86
+ onkeydown={onclick ? handleKeydown : undefined}
105
87
  {...restProps}
106
88
  >
107
89
  {#if useOldAPI}
108
90
  <!-- Old API: Using title/description/image props -->
109
- {#if image}
110
- <img
111
- src={image}
112
- alt={title}
113
- class="w-full h-48 object-cover rounded-lg mb-4"
114
- />
115
- {/if}
116
-
117
- {#if title}
118
- <h3 class={titleClasses()}>{title}</h3>
91
+ {#if title || description}
92
+ <CardHeader {title} {description} />
119
93
  {/if}
120
94
 
121
- {#if description}
122
- <p class={descriptionClasses()}>{description}</p>
123
- {/if}
124
-
125
- {#if children}
126
- {@render children()}
95
+ {#if image || children}
96
+ <CardContent {image} imageAlt={title}>
97
+ {#if children}
98
+ {@render children()}
99
+ {/if}
100
+ </CardContent>
127
101
  {/if}
128
102
  {:else}
129
103
  <!-- New API: Using compound components (CardHeader, CardContent, CardFooter) -->
130
104
  {#if children}
131
- {@render children()}
105
+ <CardContent className="pt-6">
106
+ {@render children()}
107
+ </CardContent>
132
108
  {/if}
133
109
  {/if}
134
110
  </div>
@@ -2,23 +2,30 @@
2
2
  import type { Snippet } from "svelte";
3
3
 
4
4
  interface Props {
5
+ image?: string;
6
+ imageAlt?: string;
5
7
  className?: string;
6
8
  children?: Snippet;
7
9
  }
8
10
 
9
11
  let {
12
+ image = "",
13
+ imageAlt = "",
10
14
  className = "",
11
15
  children,
12
16
  ...restProps
13
17
  }: Props = $props();
14
18
  </script>
15
19
 
16
- <div
17
- class="p-6 pt-0 {className}"
18
- {...restProps}
19
- >
20
+ <div class="p-6 pt-0 {className}" {...restProps}>
21
+ {#if image}
22
+ <img
23
+ src={image}
24
+ alt={imageAlt}
25
+ class="w-full h-48 object-cover rounded-lg mb-4"
26
+ />
27
+ {/if}
20
28
  {#if children}
21
29
  {@render children()}
22
30
  {/if}
23
31
  </div>
24
-
@@ -1,5 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  interface Props {
3
+ image?: string;
4
+ imageAlt?: string;
3
5
  className?: string;
4
6
  children?: Snippet;
5
7
  }
@@ -2,23 +2,40 @@
2
2
  import type { Snippet } from "svelte";
3
3
 
4
4
  interface Props {
5
+ title?: string;
6
+ description?: string;
7
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
5
8
  className?: string;
6
9
  children?: Snippet;
7
10
  }
8
11
 
9
12
  let {
13
+ title = "",
14
+ description = "",
15
+ level = 3,
10
16
  className = "",
11
17
  children,
12
18
  ...restProps
13
19
  }: Props = $props();
20
+
21
+ const headingTag = $derived(`h${level}`);
22
+ const headingClasses = $derived(() => {
23
+ return "text-2xl font-semibold leading-none tracking-tight text-headline";
24
+ });
14
25
  </script>
15
26
 
16
- <header
17
- class="flex flex-col space-y-1.5 p-6 {className}"
18
- {...restProps}
19
- >
27
+ <header class="flex flex-col space-y-1.5 p-6 {className}" {...restProps}>
28
+ {#if title}
29
+ <svelte:element this={headingTag} class={headingClasses()}>
30
+ {title}
31
+ </svelte:element>
32
+ {/if}
20
33
  {#if children}
21
34
  {@render children()}
22
35
  {/if}
36
+ {#if description}
37
+ <p class="text-sm text-description">
38
+ {description}
39
+ </p>
40
+ {/if}
23
41
  </header>
24
-
@@ -1,5 +1,8 @@
1
1
  import type { Snippet } from "svelte";
2
2
  interface Props {
3
+ title?: string;
4
+ description?: string;
5
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
3
6
  className?: string;
4
7
  children?: Snippet;
5
8
  }
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Input from "./Input.svelte";
3
+ import { onMount, onDestroy } from "svelte";
3
4
 
4
5
  interface Props {
5
6
  value?: string;
@@ -18,11 +19,83 @@
18
19
  ...restProps
19
20
  }: Props = $props();
20
21
 
22
+ let isOpen = $state(false);
23
+ let pickerContainer: HTMLDivElement;
24
+ let colorMap: HTMLCanvasElement;
25
+ let colorMapContainer: HTMLDivElement;
26
+ let hueSlider: HTMLInputElement;
27
+ let isDragging = $state(false);
28
+
29
+ // HSL values (0-360, 0-100, 0-100)
30
+ let hue = $state(0);
31
+ let saturation = $state(100);
32
+ let lightness = $state(50);
33
+
21
34
  function isValidHex(hex: string): boolean {
22
35
  const hexPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
23
36
  return hexPattern.test(hex);
24
37
  }
25
38
 
39
+ function hexToHsl(hex: string): [number, number, number] {
40
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
41
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
42
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
43
+
44
+ const max = Math.max(r, g, b);
45
+ const min = Math.min(r, g, b);
46
+ let h = 0;
47
+ let s = 0;
48
+ const l = (max + min) / 2;
49
+
50
+ if (max !== min) {
51
+ const d = max - min;
52
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
53
+ switch (max) {
54
+ case r:
55
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
56
+ break;
57
+ case g:
58
+ h = ((b - r) / d + 2) / 6;
59
+ break;
60
+ case b:
61
+ h = ((r - g) / d + 4) / 6;
62
+ break;
63
+ }
64
+ }
65
+
66
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
67
+ }
68
+
69
+ function hslToHex(h: number, s: number, l: number): string {
70
+ l /= 100;
71
+ const a = (s * Math.min(l, 1 - l)) / 100;
72
+ const f = (n: number) => {
73
+ const k = (n + h / 30) % 12;
74
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
75
+ return Math.round(255 * color)
76
+ .toString(16)
77
+ .padStart(2, "0");
78
+ };
79
+ return `#${f(0)}${f(8)}${f(4)}`;
80
+ }
81
+
82
+ function updateFromHex(hex: string) {
83
+ if (isValidHex(hex)) {
84
+ const [h, s, l] = hexToHsl(hex);
85
+ hue = h;
86
+ saturation = s;
87
+ lightness = l;
88
+ }
89
+ }
90
+
91
+ function updateHexFromHsl() {
92
+ const hex = hslToHex(hue, saturation, lightness);
93
+ value = hex;
94
+ if (onchange) {
95
+ onchange(new Event("change"));
96
+ }
97
+ }
98
+
26
99
  const variant = $derived(() => {
27
100
  if (value && !isValidHex(value)) {
28
101
  return "error";
@@ -39,6 +112,7 @@
39
112
 
40
113
  function handleInput(event: Event) {
41
114
  if (onchange) onchange(event);
115
+ updateFromHex(value);
42
116
  }
43
117
 
44
118
  function handleBlur(event: Event) {
@@ -51,9 +125,134 @@
51
125
 
52
126
  if (hexValue === "" || isValidHex(hexValue)) {
53
127
  value = hexValue;
128
+ updateFromHex(hexValue);
54
129
  if (onchange) onchange(event);
55
130
  }
56
131
  }
132
+
133
+ function togglePicker() {
134
+ if (!disabled) {
135
+ isOpen = !isOpen;
136
+ if (isOpen && value && isValidHex(value)) {
137
+ updateFromHex(value);
138
+ }
139
+ }
140
+ }
141
+
142
+ function drawColorMap() {
143
+ if (!colorMap) return;
144
+ const ctx = colorMap.getContext("2d");
145
+ if (!ctx) return;
146
+
147
+ const width = colorMap.width;
148
+ const height = colorMap.height;
149
+
150
+ // Draw saturation/lightness gradient
151
+ for (let x = 0; x < width; x++) {
152
+ for (let y = 0; y < height; y++) {
153
+ const s = (x / width) * 100;
154
+ const l = 100 - (y / height) * 100;
155
+ const hex = hslToHex(hue, s, l);
156
+ ctx.fillStyle = hex;
157
+ ctx.fillRect(x, y, 1, 1);
158
+ }
159
+ }
160
+ }
161
+
162
+ function handleColorMapClick(event: MouseEvent) {
163
+ if (!colorMapContainer || !colorMap) return;
164
+ const rect = colorMapContainer.getBoundingClientRect();
165
+ const x = event.clientX - rect.left;
166
+ const y = event.clientY - rect.top;
167
+
168
+ saturation = Math.max(0, Math.min(100, (x / rect.width) * 100));
169
+ lightness = Math.max(0, Math.min(100, 100 - (y / rect.height) * 100));
170
+
171
+ updateHexFromHsl();
172
+ drawColorMap();
173
+ }
174
+
175
+ function handleColorMapDrag(event: MouseEvent) {
176
+ if (isDragging) {
177
+ handleColorMapClick(event);
178
+ }
179
+ }
180
+
181
+ function handleHueChange(event: Event) {
182
+ const target = event.target as HTMLInputElement;
183
+ hue = parseInt(target.value);
184
+ updateHexFromHsl();
185
+ drawColorMap();
186
+ }
187
+
188
+ $effect(() => {
189
+ if (isOpen && colorMap) {
190
+ // Use setTimeout to ensure canvas is rendered
191
+ setTimeout(() => {
192
+ drawColorMap();
193
+ }, 0);
194
+ }
195
+ });
196
+
197
+ $effect(() => {
198
+ if (hue !== undefined && colorMap && isOpen) {
199
+ drawColorMap();
200
+ }
201
+ });
202
+
203
+ $effect(() => {
204
+ if (value && isValidHex(value) && !isDragging) {
205
+ updateFromHex(value);
206
+ }
207
+ });
208
+
209
+ function handleClickOutside(event: MouseEvent) {
210
+ if (
211
+ pickerContainer &&
212
+ !pickerContainer.contains(event.target as Node) &&
213
+ !(event.target as HTMLElement).closest(
214
+ '[aria-label="Open color picker"]',
215
+ )
216
+ ) {
217
+ isOpen = false;
218
+ }
219
+ }
220
+
221
+ onMount(() => {
222
+ if (value && isValidHex(value)) {
223
+ updateFromHex(value);
224
+ }
225
+ window.addEventListener("mousedown", handleClickOutside);
226
+ window.addEventListener("mouseup", () => {
227
+ isDragging = false;
228
+ });
229
+ window.addEventListener("mousemove", handleColorMapDrag);
230
+ });
231
+
232
+ onDestroy(() => {
233
+ window.removeEventListener("mousedown", handleClickOutside);
234
+ window.removeEventListener("mouseup", () => {});
235
+ window.removeEventListener("mousemove", handleColorMapDrag);
236
+ });
237
+
238
+ const displayColor = $derived(() => {
239
+ if (value && isValidHex(value)) {
240
+ return value;
241
+ }
242
+ return placeholder;
243
+ });
244
+
245
+ const currentHslColor = $derived(() => {
246
+ return `hsl(${hue}, 100%, 50%)`;
247
+ });
248
+
249
+ const pickerIndicatorX = $derived(() => {
250
+ return `${(saturation / 100) * 100}%`;
251
+ });
252
+
253
+ const pickerIndicatorY = $derived(() => {
254
+ return `${100 - (lightness / 100) * 100}%`;
255
+ });
57
256
  </script>
58
257
 
59
258
  <div {...restProps}>
@@ -71,12 +270,77 @@
71
270
  aria-label="Hex color input"
72
271
  />
73
272
  </div>
74
- {#if value && isValidHex(value)}
75
- <div
76
- class="w-8 h-8 rounded border-2 border-base-300 shrink-0 mt-6"
77
- style="background-color: {value};"
78
- aria-label="Color preview"
79
- ></div>
80
- {/if}
273
+ <div class="relative shrink-0 mt-6">
274
+ <button
275
+ type="button"
276
+ onclick={togglePicker}
277
+ {disabled}
278
+ class="w-11 h-11 rounded-lg border-2 border-card shrink-0 cursor-pointer hover:ring-2 hover:ring-brand-500 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
279
+ style="background-color: {displayColor()};"
280
+ aria-label="Open color picker"
281
+ aria-expanded={isOpen}
282
+ ></button>
283
+
284
+ {#if isOpen}
285
+ <div
286
+ bind:this={pickerContainer}
287
+ class="absolute top-12 right-0 z-50 bg-input rounded-2xl shadow-lg p-4 w-80"
288
+ role="dialog"
289
+ aria-label="Color picker"
290
+ >
291
+ <div class="space-y-4">
292
+ <!-- Color Map -->
293
+ <div
294
+ bind:this={colorMapContainer}
295
+ class="relative w-full h-48 rounded-xl overflow-hidden cursor-crosshair"
296
+ onmousedown={(e) => {
297
+ isDragging = true;
298
+ handleColorMapClick(e);
299
+ }}
300
+ role="button"
301
+ tabindex="0"
302
+ >
303
+ <canvas
304
+ bind:this={colorMap}
305
+ width={256}
306
+ height={192}
307
+ class="w-full h-full"
308
+ ></canvas>
309
+ <!-- Color indicator -->
310
+ <div
311
+ class="absolute w-4 h-4 border-2 border-white rounded-full shadow-lg pointer-events-none transform -translate-x-1/2 -translate-y-1/2"
312
+ style="left: {pickerIndicatorX()}; top: {pickerIndicatorY()};"
313
+ ></div>
314
+ </div>
315
+
316
+ <!-- Hue Slider -->
317
+ <div class="space-y-2">
318
+ <div
319
+ class="relative h-6 rounded-lg overflow-hidden"
320
+ >
321
+ <div
322
+ class="absolute inset-0"
323
+ style="background: linear-gradient(to right, hsl(0,100%,50%), hsl(60,100%,50%), hsl(120,100%,50%), hsl(180,100%,50%), hsl(240,100%,50%), hsl(300,100%,50%), hsl(360,100%,50%));"
324
+ ></div>
325
+ <input
326
+ bind:this={hueSlider}
327
+ type="range"
328
+ min="0"
329
+ max="360"
330
+ value={hue}
331
+ oninput={handleHueChange}
332
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
333
+ aria-label="Hue slider"
334
+ />
335
+ <div
336
+ class="absolute top-0 bottom-0 w-3 bg-card shadow-md pointer-events-none my-0.25 rounded-full border border-base-950"
337
+ style="left: {(hue / 360) * 100}%;"
338
+ ></div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ {/if}
344
+ </div>
81
345
  </div>
82
346
  </div>
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { ButtonVariant, SizeVariant } from "../../types/variants.js";
4
+
5
+ interface Props {
6
+ /** Visual style variant. */
7
+ variant?: ButtonVariant;
8
+ /** Size variant controlling padding and radius. */
9
+ size?: SizeVariant;
10
+ /** Whether the button is disabled. */
11
+ disabled?: boolean;
12
+ /** Button type attribute. */
13
+ type?: "button" | "submit" | "reset";
14
+ /** Accessible label for icon-only buttons. */
15
+ label?: string;
16
+ /** Click handler for native click events. */
17
+ onclick?: (event: MouseEvent) => void;
18
+ /** Icon content. */
19
+ children?: Snippet;
20
+ }
21
+
22
+ let {
23
+ variant = "primary",
24
+ size = "md",
25
+ disabled = false,
26
+ type = "button",
27
+ label = "",
28
+ onclick,
29
+ children,
30
+ ...restProps
31
+ }: Props = $props();
32
+
33
+ const sizeClass = $derived(() => {
34
+ if (size === "sm") {
35
+ return {
36
+ padding: "p-2",
37
+ radius: "rounded-md",
38
+ };
39
+ } else if (size === "lg") {
40
+ return {
41
+ padding: "p-3",
42
+ radius: "rounded-xl",
43
+ };
44
+ } else {
45
+ return {
46
+ padding: "p-2.5",
47
+ radius: "rounded-lg",
48
+ };
49
+ }
50
+ });
51
+
52
+ const variantClass = $derived(() => {
53
+ return variant === "primary"
54
+ ? "bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
55
+ : variant === "secondary"
56
+ ? "bg-action-secondary text-headline hover:bg-action-secondary-hover active:bg-action-secondary-active focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
57
+ : variant === "danger"
58
+ ? "bg-action-danger text-inverse hover:bg-action-danger-hover active:bg-action-danger-active focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
59
+ : variant === "ghost"
60
+ ? "bg-transparent text-headline hover:bg-base-100 active:bg-base-200 focus:ring-2 focus:ring-base-500 focus:ring-offset-2 disabled:text-disabled"
61
+ : variant === "outline"
62
+ ? "bg-transparent border-2 border-action-primary text-headline hover:bg-action-secondary hover:text-action-primary active:bg-action-primary-active active:text-action-primary focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 disabled:border-disabled disabled:text-disabled"
63
+ : variant === "link"
64
+ ? "text-brand-700 active:text-brand-700 underline-offset-4 hover:underline focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 disabled:text-disabled disabled:no-underline"
65
+ : "bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active focus:ring-2 focus:ring-brand-500 focus:ring-offset-2";
66
+ });
67
+
68
+ const buttonClasses = $derived(() => {
69
+ const sizeStyles = sizeClass();
70
+ const baseClasses =
71
+ "inline-flex items-center justify-center transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none";
72
+
73
+ return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.radius} ${variantClass()}`.trim();
74
+ });
75
+ </script>
76
+
77
+ <button
78
+ {type}
79
+ class={buttonClasses()}
80
+ {disabled}
81
+ {onclick}
82
+ aria-label={label || undefined}
83
+ {...restProps}
84
+ >
85
+ {#if children}
86
+ {@render children()}
87
+ {/if}
88
+ </button>
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ButtonVariant, SizeVariant } from "../../types/variants.js";
3
+ interface Props {
4
+ /** Visual style variant. */
5
+ variant?: ButtonVariant;
6
+ /** Size variant controlling padding and radius. */
7
+ size?: SizeVariant;
8
+ /** Whether the button is disabled. */
9
+ disabled?: boolean;
10
+ /** Button type attribute. */
11
+ type?: "button" | "submit" | "reset";
12
+ /** Accessible label for icon-only buttons. */
13
+ label?: string;
14
+ /** Click handler for native click events. */
15
+ onclick?: (event: MouseEvent) => void;
16
+ /** Icon content. */
17
+ children?: Snippet;
18
+ }
19
+ declare const IconButton: import("svelte").Component<Props, {}, "">;
20
+ type IconButton = ReturnType<typeof IconButton>;
21
+ export default IconButton;