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
@@ -21,10 +21,12 @@
21
21
  variant?: SemanticVariant;
22
22
  message?: string;
23
23
  oninput?: (event: Event) => void;
24
+ onblur?: (event: Event) => void;
25
+ "aria-label"?: string;
24
26
  }
25
27
 
26
28
  let {
27
- value = "",
29
+ value = $bindable(""),
28
30
  type = "text",
29
31
  name = "",
30
32
  label = "",
@@ -34,6 +36,7 @@
34
36
  variant = "default",
35
37
  message = "",
36
38
  oninput,
39
+ onblur,
37
40
  ...restProps
38
41
  }: Props = $props();
39
42
 
@@ -74,7 +77,7 @@
74
77
  const inputClasses = $derived(() => {
75
78
  const sizeStyles = sizeClass();
76
79
  const baseClasses =
77
- "w-full bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed";
80
+ "w-full min-w-48 bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed";
78
81
 
79
82
  return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
80
83
  });
@@ -102,9 +105,6 @@
102
105
  });
103
106
 
104
107
  function handleInput(event: Event) {
105
- const target = event.target as HTMLInputElement;
106
- value = target.value;
107
-
108
108
  if (oninput) {
109
109
  oninput(event);
110
110
  }
@@ -119,11 +119,12 @@
119
119
  id={inputId}
120
120
  {type}
121
121
  {name}
122
- {value}
122
+ bind:value
123
123
  {placeholder}
124
124
  {disabled}
125
125
  class={inputClasses()}
126
126
  oninput={handleInput}
127
+ {onblur}
127
128
  aria-invalid={variant === "error" ? "true" : undefined}
128
129
  aria-describedby={message ? `${inputId}-message` : undefined}
129
130
  {...restProps}
@@ -10,7 +10,9 @@ interface Props {
10
10
  variant?: SemanticVariant;
11
11
  message?: string;
12
12
  oninput?: (event: Event) => void;
13
+ onblur?: (event: Event) => void;
14
+ "aria-label"?: string;
13
15
  }
14
- declare const Input: import("svelte").Component<Props, {}, "">;
16
+ declare const Input: import("svelte").Component<Props, {}, "value">;
15
17
  type Input = ReturnType<typeof Input>;
16
18
  export default Input;
@@ -41,9 +41,7 @@
41
41
 
42
42
  <div
43
43
  id={progressId}
44
- class="w-full bg-base-200 rounded-full overflow-hidden {sizeClasses[
45
- size
46
- ]}"
44
+ class="w-full bg-input rounded-full overflow-hidden {sizeClasses[size]}"
47
45
  role="progressbar"
48
46
  aria-valuenow={value}
49
47
  aria-valuemin="0"
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Dropdown from "../molecules/Dropdown.svelte";
3
+ import Button from "./Button.svelte";
3
4
  import {
4
5
  ChevronDown,
5
6
  CheckCircle,
@@ -22,6 +23,16 @@
22
23
  label: string;
23
24
  disabled?: boolean;
24
25
  }>;
26
+ /** Enables the search input inside the dropdown. */
27
+ searchable?: boolean;
28
+ /** Placeholder for the search input. */
29
+ searchPlaceholder?: string;
30
+ /** Max height for the options list (supports any valid CSS length). */
31
+ maxMenuHeight?: string;
32
+ /** Width for the dropdown menu (supports any valid CSS length). */
33
+ menuWidth?: string;
34
+ /** Message shown when no options match the search. */
35
+ noResultsText?: string;
25
36
  placeholder?: string;
26
37
  label?: string;
27
38
  disabled?: boolean;
@@ -34,6 +45,11 @@
34
45
  let {
35
46
  value = undefined,
36
47
  options = [],
48
+ searchable = true,
49
+ searchPlaceholder = "Search options",
50
+ maxMenuHeight = "60vh",
51
+ menuWidth = "100%",
52
+ noResultsText = "No results found",
37
53
  placeholder = "Select an option",
38
54
  label = "",
39
55
  disabled = false,
@@ -45,6 +61,8 @@
45
61
  }: Props = $props();
46
62
 
47
63
  let isOpen = $state(false);
64
+ let selectContainer: HTMLDivElement;
65
+ let searchQuery = $state("");
48
66
 
49
67
  const sizeClass = $derived(() => {
50
68
  if (size === "sm") {
@@ -92,13 +110,13 @@
92
110
 
93
111
  const messageClasses = $derived(() => {
94
112
  if (variant === "error") {
95
- return "text-error text-sm mt-1 flex items-center gap-1.5";
113
+ return "text-error text-sm mt-1 flex items-center gap-1.5 w-full";
96
114
  } else if (variant === "success") {
97
- return "text-success text-sm mt-1 flex items-center gap-1.5";
115
+ return "text-success text-sm mt-1 flex items-center gap-1.5 w-full";
98
116
  } else if (variant === "warning") {
99
- return "text-warning text-sm mt-1 flex items-center gap-1.5";
117
+ return "text-warning text-sm mt-1 flex items-center gap-1.5 w-full";
100
118
  }
101
- return "text-description text-sm mt-1 flex items-center gap-1.5";
119
+ return "text-description text-sm mt-1 flex items-center gap-1.5 w-full";
102
120
  });
103
121
 
104
122
  const getIcon = $derived(() => {
@@ -124,6 +142,33 @@
124
142
  return value === undefined || value === null || value === "";
125
143
  });
126
144
 
145
+ const normalizedOptions = $derived(() => {
146
+ const fallbackOptions = (restProps as { options?: Props["options"] })
147
+ .options;
148
+ return Array.from(options ?? fallbackOptions ?? []);
149
+ });
150
+
151
+ const hasSearchQuery = $derived(() => {
152
+ return searchQuery.trim().length > 0;
153
+ });
154
+
155
+ const filteredOptions = $derived(() => {
156
+ const query = searchQuery.trim().toLowerCase();
157
+ const availableOptions = normalizedOptions();
158
+ if (!searchable || query.length === 0) {
159
+ return availableOptions;
160
+ }
161
+ return availableOptions.filter((option) =>
162
+ option.label.toLowerCase().includes(query),
163
+ );
164
+ });
165
+
166
+ $effect(() => {
167
+ if (!isOpen && searchQuery.length > 0) {
168
+ searchQuery = "";
169
+ }
170
+ });
171
+
127
172
  function handleOptionClick(optionValue: string | number) {
128
173
  if (disabled) return;
129
174
  value = optionValue;
@@ -146,9 +191,15 @@
146
191
  }
147
192
 
148
193
  function handleClickOutside(event: MouseEvent) {
194
+ if (!isOpen) return;
195
+
196
+ const target = event.target as HTMLElement;
197
+ const clickedSelectContainer = target.closest(".select-container");
198
+
199
+ // Close if clicking outside any select container, or clicking on a different select
149
200
  if (
150
- isOpen &&
151
- !(event.target as HTMLElement).closest(".select-container")
201
+ !clickedSelectContainer ||
202
+ clickedSelectContainer !== selectContainer
152
203
  ) {
153
204
  isOpen = false;
154
205
  }
@@ -163,12 +214,18 @@
163
214
 
164
215
  <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
165
216
 
166
- <div class="w-full select-container">
217
+ <div bind:this={selectContainer} class="w-full select-container">
167
218
  {#if label}
168
219
  <label for={selectId} class={labelClasses()}>{label}</label>
169
220
  {/if}
170
221
 
171
- <Dropdown {isOpen} placement="bottom-start">
222
+ <Dropdown
223
+ {isOpen}
224
+ placement="bottom-start"
225
+ selectedValue={value}
226
+ onOptionClick={handleOptionClick}
227
+ ariaLabel="Select options"
228
+ >
172
229
  {#snippet trigger()}
173
230
  <button
174
231
  type="button"
@@ -181,7 +238,7 @@
181
238
  aria-describedby={message ? `${selectId}-message` : undefined}
182
239
  >
183
240
  <span
184
- class="text-left flex-1 {isEmpty()
241
+ class="text-left flex-1 truncate {isEmpty()
185
242
  ? 'text-description'
186
243
  : 'text-body'}"
187
244
  >
@@ -199,21 +256,49 @@
199
256
  </button>
200
257
  {/snippet}
201
258
  {#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}
259
+ <div class="px-2 pb-2 pt-1" style:width={menuWidth}>
260
+ {#if searchable}
261
+ <div class="px-1 pb-2">
262
+ <input
263
+ type="text"
264
+ class="w-full rounded-md bg-input hover:bg-input-hover focus:bg-input-focus text-body placeholder:text-description px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed"
265
+ placeholder={searchPlaceholder}
266
+ bind:value={searchQuery}
267
+ aria-label={searchPlaceholder}
268
+ {disabled}
269
+ />
270
+ </div>
271
+ {/if}
272
+ <div
273
+ class="overflow-y-auto px-1"
274
+ style:max-height={maxMenuHeight}
275
+ >
276
+ {#if filteredOptions().length > 0}
277
+ {#each filteredOptions() as option (option.value)}
278
+ {@const buttonRestProps = {
279
+ "data-value": String(option.value),
280
+ } as Record<string, string>}
281
+ <div class="w-full my-0.5">
282
+ <Button
283
+ variant={value === option.value
284
+ ? "outline"
285
+ : "ghost"}
286
+ size="sm"
287
+ isFullWidth={true}
288
+ disabled={option.disabled}
289
+ onclick={() => handleOptionClick(option.value)}
290
+ {...buttonRestProps}
291
+ >
292
+ {option.label}
293
+ </Button>
294
+ </div>
295
+ {/each}
296
+ {:else if hasSearchQuery()}
297
+ <div class="px-3 py-2 text-sm text-description">
298
+ {noResultsText}
299
+ </div>
300
+ {/if}
301
+ </div>
217
302
  </div>
218
303
  {/snippet}
219
304
  </Dropdown>
@@ -5,6 +5,16 @@ interface Props {
5
5
  label: string;
6
6
  disabled?: boolean;
7
7
  }>;
8
+ /** Enables the search input inside the dropdown. */
9
+ searchable?: boolean;
10
+ /** Placeholder for the search input. */
11
+ searchPlaceholder?: string;
12
+ /** Max height for the options list (supports any valid CSS length). */
13
+ maxMenuHeight?: string;
14
+ /** Width for the dropdown menu (supports any valid CSS length). */
15
+ menuWidth?: string;
16
+ /** Message shown when no options match the search. */
17
+ noResultsText?: string;
8
18
  placeholder?: string;
9
19
  label?: string;
10
20
  disabled?: boolean;
@@ -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
  }
@@ -1,12 +1,11 @@
1
1
  export { default as Toggle } from './Toggle.svelte';
2
2
  export { default as Badge } from './Badge.svelte';
3
3
  export { default as Button } from './Button.svelte';
4
+ export { default as IconButton } from './IconButton.svelte';
4
5
  export { default as Card } from './Card.svelte';
5
6
  export { default as CardHeader } from './CardHeader.svelte';
6
7
  export { default as CardContent } from './CardContent.svelte';
7
8
  export { default as CardFooter } from './CardFooter.svelte';
8
- export { default as CardTitle } from './CardTitle.svelte';
9
- export { default as CardDescription } from './CardDescription.svelte';
10
9
  export { default as Input } from './Input.svelte';
11
10
  export { default as Textarea } from './Textarea.svelte';
12
11
  export { default as Select } from './Select.svelte';
@@ -1,12 +1,11 @@
1
1
  export { default as Toggle } from './Toggle.svelte';
2
2
  export { default as Badge } from './Badge.svelte';
3
3
  export { default as Button } from './Button.svelte';
4
+ export { default as IconButton } from './IconButton.svelte';
4
5
  export { default as Card } from './Card.svelte';
5
6
  export { default as CardHeader } from './CardHeader.svelte';
6
7
  export { default as CardContent } from './CardContent.svelte';
7
8
  export { default as CardFooter } from './CardFooter.svelte';
8
- export { default as CardTitle } from './CardTitle.svelte';
9
- export { default as CardDescription } from './CardDescription.svelte';
10
9
  export { default as Input } from './Input.svelte';
11
10
  export { default as Textarea } from './Textarea.svelte';
12
11
  export { default as Select } from './Select.svelte';
@@ -1,12 +1,11 @@
1
1
  export { default as Toggle } from './Toggle.svelte';
2
2
  export { default as Badge } from './Badge.svelte';
3
3
  export { default as Button } from './Button.svelte';
4
+ export { default as IconButton } from './IconButton.svelte';
4
5
  export { default as Card } from './Card.svelte';
5
6
  export { default as CardHeader } from './CardHeader.svelte';
6
7
  export { default as CardContent } from './CardContent.svelte';
7
8
  export { default as CardFooter } from './CardFooter.svelte';
8
- export { default as CardTitle } from './CardTitle.svelte';
9
- export { default as CardDescription } from './CardDescription.svelte';
10
9
  export { default as Input } from './Input.svelte';
11
10
  export { default as Textarea } from './Textarea.svelte';
12
11
  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,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,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,14 @@ 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 Section } from './Section.svelte';
15
+ export { default as Sidebar } from './Sidebar.svelte';
8
16
  export { default as SlideUp } from './SlideUp.svelte';
9
17
  export { default as Tabs } from './Tabs.svelte';
10
18
  //# 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,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>