zabi-components 5.0.18 → 5.0.20

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 (54) hide show
  1. package/dist/atoms/Button.svelte +6 -3
  2. package/dist/atoms/Button.svelte.d.ts +1 -0
  3. package/dist/atoms/Card.svelte +23 -47
  4. package/dist/atoms/CardContent.svelte +12 -5
  5. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  6. package/dist/atoms/CardHeader.svelte +22 -5
  7. package/dist/atoms/CardHeader.svelte.d.ts +3 -0
  8. package/dist/atoms/ColorPicker.svelte +271 -7
  9. package/dist/atoms/Input.svelte +7 -6
  10. package/dist/atoms/Input.svelte.d.ts +3 -1
  11. package/dist/atoms/Progress.svelte +1 -3
  12. package/dist/atoms/Select.svelte +17 -22
  13. package/dist/atoms/Skeleton.svelte +12 -2
  14. package/dist/atoms/Skeleton.svelte.d.ts +1 -0
  15. package/dist/atoms/Tooltip.svelte +15 -17
  16. package/dist/atoms/index.d.ts +0 -2
  17. package/dist/atoms/index.js +0 -2
  18. package/dist/components/atoms/index.d.ts +0 -2
  19. package/dist/components/atoms/index.d.ts.map +1 -1
  20. package/dist/components/molecules/index.d.ts +7 -0
  21. package/dist/components/molecules/index.d.ts.map +1 -1
  22. package/dist/molecules/Dropdown.svelte +101 -14
  23. package/dist/molecules/Dropdown.svelte.d.ts +7 -0
  24. package/dist/molecules/ImageUpload.svelte +4 -4
  25. package/dist/molecules/Modal.svelte +43 -27
  26. package/dist/molecules/Modal.svelte.d.ts +1 -0
  27. package/dist/molecules/NavigationMenu.svelte +158 -0
  28. package/dist/molecules/NavigationMenu.svelte.d.ts +25 -0
  29. package/dist/molecules/NavigationMenuContent.svelte +83 -0
  30. package/dist/molecules/NavigationMenuContent.svelte.d.ts +9 -0
  31. package/dist/molecules/NavigationMenuItem.svelte +36 -0
  32. package/dist/molecules/NavigationMenuItem.svelte.d.ts +9 -0
  33. package/dist/molecules/NavigationMenuLink.svelte +53 -0
  34. package/dist/molecules/NavigationMenuLink.svelte.d.ts +10 -0
  35. package/dist/molecules/NavigationMenuList.svelte +31 -0
  36. package/dist/molecules/NavigationMenuList.svelte.d.ts +8 -0
  37. package/dist/molecules/NavigationMenuTrigger.svelte +81 -0
  38. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +9 -0
  39. package/dist/molecules/Sidebar.svelte +324 -0
  40. package/dist/molecules/Sidebar.svelte.d.ts +30 -0
  41. package/dist/molecules/SlideUp.svelte +2 -2
  42. package/dist/molecules/index.d.ts +7 -0
  43. package/dist/molecules/index.js +7 -0
  44. package/dist/routes/lib/focus-utils.d.ts +23 -0
  45. package/dist/routes/lib/focus-utils.ts +112 -0
  46. package/dist/zabi-components-colors.css +2 -2
  47. package/dist/zabi-components-theme-only.css +2 -2
  48. package/dist/zabi-components-theme.css +2 -2
  49. package/dist/zabi-components.css +192 -27
  50. package/package.json +4 -3
  51. package/dist/atoms/CardDescription.svelte +0 -24
  52. package/dist/atoms/CardDescription.svelte.d.ts +0 -8
  53. package/dist/atoms/CardTitle.svelte +0 -33
  54. package/dist/atoms/CardTitle.svelte.d.ts +0 -9
@@ -45,6 +45,7 @@
45
45
  }: Props = $props();
46
46
 
47
47
  let isOpen = $state(false);
48
+ let selectContainer: HTMLDivElement;
48
49
 
49
50
  const sizeClass = $derived(() => {
50
51
  if (size === "sm") {
@@ -146,9 +147,15 @@
146
147
  }
147
148
 
148
149
  function handleClickOutside(event: MouseEvent) {
150
+ if (!isOpen) return;
151
+
152
+ const target = event.target as HTMLElement;
153
+ const clickedSelectContainer = target.closest(".select-container");
154
+
155
+ // Close if clicking outside any select container, or clicking on a different select
149
156
  if (
150
- isOpen &&
151
- !(event.target as HTMLElement).closest(".select-container")
157
+ !clickedSelectContainer ||
158
+ clickedSelectContainer !== selectContainer
152
159
  ) {
153
160
  isOpen = false;
154
161
  }
@@ -163,12 +170,18 @@
163
170
 
164
171
  <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
165
172
 
166
- <div class="w-full select-container">
173
+ <div bind:this={selectContainer} class="w-full select-container">
167
174
  {#if label}
168
175
  <label for={selectId} class={labelClasses()}>{label}</label>
169
176
  {/if}
170
177
 
171
- <Dropdown {isOpen} placement="bottom-start">
178
+ <Dropdown
179
+ {isOpen}
180
+ placement="bottom-start"
181
+ selectedValue={value}
182
+ {options}
183
+ onOptionClick={handleOptionClick}
184
+ >
172
185
  {#snippet trigger()}
173
186
  <button
174
187
  type="button"
@@ -198,24 +211,6 @@
198
211
  />
199
212
  </button>
200
213
  {/snippet}
201
- {#snippet children()}
202
- <div class="px-2 py-2">
203
- {#each options as option (option.value)}
204
- <button
205
- type="button"
206
- class="w-full text-left px-4 py-2 text-body hover:bg-base-100 transition-colors rounded-md my-0.5 {option.disabled
207
- ? 'opacity-50 cursor-not-allowed'
208
- : 'cursor-pointer'} {value === option.value
209
- ? 'bg-base-100'
210
- : ''}"
211
- onclick={() => handleOptionClick(option.value)}
212
- disabled={option.disabled}
213
- >
214
- {option.label}
215
- </button>
216
- {/each}
217
- </div>
218
- {/snippet}
219
214
  </Dropdown>
220
215
  {#if message && variant !== "default"}
221
216
  <p id={`${selectId}-message`} class={messageClasses()} role="alert">
@@ -3,18 +3,28 @@
3
3
  width?: string;
4
4
  height?: string;
5
5
  className?: string;
6
+ "aria-label"?: string;
6
7
  }
7
8
 
8
9
  let {
9
10
  width = "100%",
10
11
  height = "1rem",
11
12
  className = "",
13
+ "aria-label": ariaLabel = "Loading...",
12
14
  ...restProps
13
15
  }: Props = $props();
16
+
17
+ const classes = $derived(
18
+ `animate-pulse bg-action-secondary rounded${className ? ` ${className}` : ""}`,
19
+ );
20
+ const styles = $derived(`width: ${width}; height: ${height};`);
14
21
  </script>
15
22
 
16
23
  <div
17
- class="animate-pulse bg-base-200 rounded {className}"
18
- style="width: {width}; height: {height};"
24
+ class={classes}
25
+ style={styles}
26
+ aria-busy="true"
27
+ aria-label={ariaLabel}
28
+ role="status"
19
29
  {...restProps}
20
30
  ></div>
@@ -2,6 +2,7 @@ interface Props {
2
2
  width?: string;
3
3
  height?: string;
4
4
  className?: string;
5
+ "aria-label"?: string;
5
6
  }
6
7
  declare const Skeleton: import("svelte").Component<Props, {}, "">;
7
8
  type Skeleton = ReturnType<typeof Skeleton>;
@@ -77,7 +77,11 @@
77
77
  onfocusout={handleBlur}
78
78
  {...restProps}
79
79
  >
80
- <div bind:this={triggerElement} id={triggerId} aria-describedby={isVisible ? tooltipId : undefined}>
80
+ <div
81
+ bind:this={triggerElement}
82
+ id={triggerId}
83
+ aria-describedby={isVisible ? tooltipId : undefined}
84
+ >
81
85
  {@render children?.()}
82
86
  </div>
83
87
 
@@ -97,10 +101,10 @@
97
101
 
98
102
  <style>
99
103
  :root {
100
- --tooltip-bg: #1f2937;
101
- --tooltip-color: white;
104
+ --tooltip-bg: var(--color-base-800);
105
+ --tooltip-color: var(--color-base-50);
102
106
  --tooltip-padding: 0.5rem 0.75rem;
103
- --tooltip-radius: 0.375rem;
107
+ --tooltip-radius: 0.5rem;
104
108
  --tooltip-font-size: 0.875rem;
105
109
  --tooltip-line-height: 1.25rem;
106
110
  --tooltip-max-width: 200px;
@@ -243,16 +247,6 @@
243
247
  }
244
248
  }
245
249
 
246
- @media (prefers-color-scheme: dark) {
247
- :root {
248
- --tooltip-bg: #374151;
249
- }
250
- }
251
-
252
- .dark :root {
253
- --tooltip-bg: #374151;
254
- }
255
-
256
250
  @media (prefers-contrast: high) {
257
251
  :root {
258
252
  --tooltip-bg: #000000;
@@ -263,16 +257,20 @@
263
257
 
264
258
  @media (prefers-reduced-motion: reduce) {
265
259
  .tooltip {
266
- transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;
260
+ transition:
261
+ opacity 0.1s ease-in-out,
262
+ visibility 0.1s ease-in-out;
267
263
  }
268
264
 
269
265
  .tooltip-container[data-placement="top"] .tooltip[data-visible="true"],
270
- .tooltip-container[data-placement="bottom"] .tooltip[data-visible="true"] {
266
+ .tooltip-container[data-placement="bottom"]
267
+ .tooltip[data-visible="true"] {
271
268
  transform: translateX(-50%) !important;
272
269
  }
273
270
 
274
271
  .tooltip-container[data-placement="left"] .tooltip[data-visible="true"],
275
- .tooltip-container[data-placement="right"] .tooltip[data-visible="true"] {
272
+ .tooltip-container[data-placement="right"]
273
+ .tooltip[data-visible="true"] {
276
274
  transform: translateY(-50%) !important;
277
275
  }
278
276
  }
@@ -5,8 +5,6 @@ export { default as Card } from './Card.svelte';
5
5
  export { default as CardHeader } from './CardHeader.svelte';
6
6
  export { default as CardContent } from './CardContent.svelte';
7
7
  export { default as CardFooter } from './CardFooter.svelte';
8
- export { default as CardTitle } from './CardTitle.svelte';
9
- export { default as CardDescription } from './CardDescription.svelte';
10
8
  export { default as Input } from './Input.svelte';
11
9
  export { default as Textarea } from './Textarea.svelte';
12
10
  export { default as Select } from './Select.svelte';
@@ -5,8 +5,6 @@ export { default as Card } from './Card.svelte';
5
5
  export { default as CardHeader } from './CardHeader.svelte';
6
6
  export { default as CardContent } from './CardContent.svelte';
7
7
  export { default as CardFooter } from './CardFooter.svelte';
8
- export { default as CardTitle } from './CardTitle.svelte';
9
- export { default as CardDescription } from './CardDescription.svelte';
10
8
  export { default as Input } from './Input.svelte';
11
9
  export { default as Textarea } from './Textarea.svelte';
12
10
  export { default as Select } from './Select.svelte';
@@ -5,8 +5,6 @@ export { default as Card } from './Card.svelte';
5
5
  export { default as CardHeader } from './CardHeader.svelte';
6
6
  export { default as CardContent } from './CardContent.svelte';
7
7
  export { default as CardFooter } from './CardFooter.svelte';
8
- export { default as CardTitle } from './CardTitle.svelte';
9
- export { default as CardDescription } from './CardDescription.svelte';
10
8
  export { default as Input } from './Input.svelte';
11
9
  export { default as Textarea } from './Textarea.svelte';
12
10
  export { default as Select } from './Select.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/atoms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/atoms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
@@ -5,6 +5,13 @@ export { default as Dropdown } from './Dropdown.svelte';
5
5
  export { default as Form } from './Form.svelte';
6
6
  export { default as ImageUpload } from './ImageUpload.svelte';
7
7
  export { default as Modal } from './Modal.svelte';
8
+ export { default as NavigationMenu } from './NavigationMenu.svelte';
9
+ export { default as NavigationMenuList } from './NavigationMenuList.svelte';
10
+ export { default as NavigationMenuItem } from './NavigationMenuItem.svelte';
11
+ export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.svelte';
12
+ export { default as NavigationMenuContent } from './NavigationMenuContent.svelte';
13
+ export { default as NavigationMenuLink } from './NavigationMenuLink.svelte';
14
+ export { default as Sidebar } from './Sidebar.svelte';
8
15
  export { default as SlideUp } from './SlideUp.svelte';
9
16
  export { default as Tabs } from './Tabs.svelte';
10
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/molecules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/molecules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC"}
@@ -1,27 +1,45 @@
1
1
  <script lang="ts">
2
+ import Button from "../atoms/Button.svelte";
3
+
2
4
  interface Props {
3
5
  isOpen?: boolean;
4
6
  placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
5
7
  ariaLabel?: string;
8
+ selectedValue?: string | number | null;
9
+ options?: Array<{
10
+ value: string | number;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }>;
14
+ onOptionClick?: (value: string | number) => void;
6
15
  }
7
16
 
8
17
  let {
9
18
  isOpen = false,
10
19
  placement = "bottom-start",
11
20
  ariaLabel = "Menu",
21
+ selectedValue = null,
22
+ options = [],
23
+ onOptionClick,
12
24
  children,
13
25
  trigger,
14
26
  ...restProps
15
27
  }: Props & { children?: any; trigger?: any } = $props();
16
28
 
17
29
  let dropdownId = `dropdown-${Math.random().toString(36).substr(2, 9)}`;
18
- let triggerElement: HTMLElement | null = null;
19
- let menuElement: HTMLElement | null = null;
30
+ let triggerElement = $state<HTMLElement | null>(null);
31
+ let menuElement = $state<HTMLElement | null>(null);
32
+ let openedViaKeyboard = $state(false);
20
33
 
21
34
  function handleKeydown(event: KeyboardEvent) {
22
35
  if (!isOpen) {
23
- if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
36
+ if (
37
+ event.key === "Enter" ||
38
+ event.key === " " ||
39
+ event.key === "ArrowDown"
40
+ ) {
24
41
  event.preventDefault();
42
+ openedViaKeyboard = true;
25
43
  isOpen = true;
26
44
  }
27
45
  return;
@@ -58,21 +76,29 @@
58
76
  function getMenuItems(): HTMLElement[] {
59
77
  if (!menuElement) return [];
60
78
  return Array.from(
61
- menuElement.querySelectorAll<HTMLElement>('[role="menuitem"], button, a')
79
+ menuElement.querySelectorAll<HTMLElement>(
80
+ '[role="menuitem"], button, a',
81
+ ),
62
82
  );
63
83
  }
64
84
 
65
85
  function focusNextItem() {
66
86
  const items = getMenuItems();
67
- const currentIndex = items.findIndex((item) => item === document.activeElement);
68
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
87
+ const currentIndex = items.findIndex(
88
+ (item) => item === document.activeElement,
89
+ );
90
+ const nextIndex =
91
+ currentIndex < items.length - 1 ? currentIndex + 1 : 0;
69
92
  items[nextIndex]?.focus();
70
93
  }
71
94
 
72
95
  function focusPreviousItem() {
73
96
  const items = getMenuItems();
74
- const currentIndex = items.findIndex((item) => item === document.activeElement);
75
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
97
+ const currentIndex = items.findIndex(
98
+ (item) => item === document.activeElement,
99
+ );
100
+ const prevIndex =
101
+ currentIndex > 0 ? currentIndex - 1 : items.length - 1;
76
102
  items[prevIndex]?.focus();
77
103
  }
78
104
 
@@ -86,11 +112,41 @@
86
112
  items[items.length - 1]?.focus();
87
113
  }
88
114
 
89
- // Focus first item when dropdown opens
115
+ // Focus first item when dropdown opens via keyboard
90
116
  $effect(() => {
91
- if (isOpen && menuElement) {
117
+ if (isOpen && menuElement && openedViaKeyboard) {
92
118
  setTimeout(() => {
93
119
  focusFirstItem();
120
+ openedViaKeyboard = false;
121
+ }, 0);
122
+ } else if (!isOpen) {
123
+ openedViaKeyboard = false;
124
+ }
125
+ });
126
+
127
+ // Mark selected items
128
+ $effect(() => {
129
+ if (isOpen && menuElement) {
130
+ // Small delay to ensure DOM is ready
131
+ setTimeout(() => {
132
+ const items = getMenuItems();
133
+ items.forEach((item) => {
134
+ if (selectedValue !== null && selectedValue !== undefined) {
135
+ const itemValue =
136
+ item.getAttribute("data-value") ||
137
+ item.textContent?.trim();
138
+ if (String(itemValue) === String(selectedValue)) {
139
+ item.setAttribute("data-selected", "true");
140
+ item.setAttribute("aria-selected", "true");
141
+ } else {
142
+ item.removeAttribute("data-selected");
143
+ item.removeAttribute("aria-selected");
144
+ }
145
+ } else {
146
+ item.removeAttribute("data-selected");
147
+ item.removeAttribute("aria-selected");
148
+ }
149
+ });
94
150
  }, 0);
95
151
  }
96
152
  });
@@ -137,8 +193,8 @@
137
193
  });
138
194
  </script>
139
195
 
140
- <div
141
- class="relative inline-block"
196
+ <div
197
+ class="relative inline-block"
142
198
  data-placement={placement}
143
199
  onkeydown={handleKeydown}
144
200
  {...restProps}
@@ -148,7 +204,7 @@
148
204
  </div>
149
205
 
150
206
  {#if isOpen}
151
- <div
207
+ <div
152
208
  bind:this={menuElement}
153
209
  id={dropdownId}
154
210
  class={dropdownContentClasses()}
@@ -156,7 +212,38 @@
156
212
  aria-label={ariaLabel}
157
213
  tabindex="-1"
158
214
  >
159
- {@render children?.()}
215
+ {#if options.length > 0}
216
+ <div class="px-2 py-2">
217
+ {#each options as option (option.value)}
218
+ {@const buttonRestProps = {
219
+ "data-value": String(option.value),
220
+ } as any}
221
+ <div class="w-full my-0.5">
222
+ <Button
223
+ variant={selectedValue === option.value
224
+ ? "outline"
225
+ : "ghost"}
226
+ size="sm"
227
+ isFullWidth={true}
228
+ disabled={option.disabled}
229
+ onclick={() => onOptionClick?.(option.value)}
230
+ {...buttonRestProps}
231
+ >
232
+ {option.label}
233
+ </Button>
234
+ </div>
235
+ {/each}
236
+ </div>
237
+ {:else if children}
238
+ {@render children?.()}
239
+ {/if}
160
240
  </div>
161
241
  {/if}
162
242
  </div>
243
+
244
+ <style>
245
+ :global([role="menu"] button) {
246
+ justify-content: flex-start;
247
+ text-align: left;
248
+ }
249
+ </style>
@@ -2,6 +2,13 @@ interface Props {
2
2
  isOpen?: boolean;
3
3
  placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
4
4
  ariaLabel?: string;
5
+ selectedValue?: string | number | null;
6
+ options?: Array<{
7
+ value: string | number;
8
+ label: string;
9
+ disabled?: boolean;
10
+ }>;
11
+ onOptionClick?: (value: string | number) => void;
5
12
  }
6
13
  type $$ComponentProps = Props & {
7
14
  children?: any;
@@ -57,10 +57,10 @@
57
57
  <img
58
58
  src={value}
59
59
  alt=""
60
- class="w-full h-32 object-cover rounded-lg border-0"
60
+ class="w-full h-32 min-w-64 object-cover rounded-2xl border-0"
61
61
  />
62
62
  <div
63
- class="absolute inset-0 bg-black/50 dark:bg-black/70 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
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"
64
64
  >
65
65
  <div class="flex gap-2">
66
66
  <Button
@@ -84,7 +84,7 @@
84
84
  </div>
85
85
  {:else}
86
86
  <div
87
- class="border-2 border-dashed border-base-200 rounded-lg p-6 text-center hover:border-brand-500 transition-colors {disabled
87
+ class="border-2 border-dashed border-input rounded-2xl min-w-64 p-6 text-center hover:border-brand-500 transition-colors {disabled
88
88
  ? 'cursor-not-allowed opacity-50'
89
89
  : 'cursor-pointer'}"
90
90
  onclick={triggerFileSelect}
@@ -96,7 +96,7 @@
96
96
  >
97
97
  <div class="space-y-3">
98
98
  <div
99
- class="w-12 h-12 mx-auto bg-base-100 rounded-lg flex items-center justify-center"
99
+ class="w-12 h-12 mx-auto bg-action-secondary rounded-lg flex items-center justify-center"
100
100
  >
101
101
  <Image size={24} class="text-description" />
102
102
  </div>
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">
2
- import { onMount } from "svelte";
3
- import { trapFocus, saveFocus, returnFocus, focusFirstElement } from "../../routes/lib/focus-utils.js";
2
+ import { trapFocus, saveFocus, returnFocus, focusFirstElement } from "../routes/lib/focus-utils.js";
3
+ import Card from "../atoms/Card.svelte";
4
+ import CardHeader from "../atoms/CardHeader.svelte";
5
+ import CardContent from "../atoms/CardContent.svelte";
6
+ import CardFooter from "../atoms/CardFooter.svelte";
4
7
 
5
8
  type Size = "sm" | "md" | "lg";
6
9
 
7
10
  interface Props {
8
11
  isOpen?: boolean;
9
12
  title?: string;
13
+ description?: string;
10
14
  size?: Size;
11
15
  onclick?: (event: Event) => void;
12
16
  onkeydown?: (event: Event) => void;
@@ -15,6 +19,7 @@
15
19
  let {
16
20
  isOpen = false,
17
21
  title = "",
22
+ description = "",
18
23
  size = "md",
19
24
  onclick,
20
25
  onkeydown,
@@ -88,38 +93,49 @@
88
93
  aria-labelledby={title ? "modal-title" : undefined}
89
94
  tabindex="-1"
90
95
  >
91
- <div
96
+ <Card
92
97
  bind:this={modalContainer}
93
- 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"
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"
94
101
  >
95
- {#if title}
96
- <div class="flex items-center justify-between px-6 pt-6 pb-4">
97
- <h2
98
- id="modal-title"
99
- class="text-2xl font-normal leading-8 text-headline tracking-normal"
100
- >
101
- {title}
102
- </h2>
103
- <button
104
- type="button"
105
- onclick={closeModal}
106
- 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"
107
- aria-label="Close"
108
- >
109
- ×
110
- </button>
111
- </div>
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>
112
126
  {/if}
113
127
 
114
- <div class="px-6 pb-6 flex-1">
115
- {@render children?.()}
116
- </div>
128
+ {#if children}
129
+ <CardContent className="flex-1">
130
+ {@render children?.()}
131
+ </CardContent>
132
+ {/if}
117
133
 
118
134
  {#if footer}
119
- <div class="flex justify-end gap-3 px-6 pb-6 pt-4">
135
+ <CardFooter className="flex justify-end gap-3 pt-4">
120
136
  {@render footer?.()}
121
- </div>
137
+ </CardFooter>
122
138
  {/if}
123
- </div>
139
+ </Card>
124
140
  </div>
125
141
  {/if}
@@ -2,6 +2,7 @@ type Size = "sm" | "md" | "lg";
2
2
  interface Props {
3
3
  isOpen?: boolean;
4
4
  title?: string;
5
+ description?: string;
5
6
  size?: Size;
6
7
  onclick?: (event: Event) => void;
7
8
  onkeydown?: (event: Event) => void;