@syscore/ui-library 1.27.3 → 2.0.0-beta-publish-workflow-beta.2

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 (169) hide show
  1. package/client/App.tsx +0 -2
  2. package/client/assets/alert/icon-clear.svg +4 -0
  3. package/client/assets/alert/icon-done.svg +3 -0
  4. package/client/assets/avatar-identity/photo.png +0 -0
  5. package/client/assets/avatar-identity/placeholder-icon.svg +8 -0
  6. package/client/assets/breadcrumb/icon-ellipsis.svg +5 -0
  7. package/client/assets/breadcrumb/icon-home.svg +3 -0
  8. package/client/assets/breadcrumb/icon-separator.svg +3 -0
  9. package/client/assets/button-v01/icon-back.svg +4 -0
  10. package/client/assets/button-v01/icon-check.svg +3 -0
  11. package/client/assets/button-v01/icon-close.svg +4 -0
  12. package/client/assets/button-v01/icon-delete.svg +3 -0
  13. package/client/assets/button-v01/icon-device.svg +3 -0
  14. package/client/assets/dropdown/icon-carrot-contract.svg +3 -0
  15. package/client/assets/dropdown/icon-carrot-expand.svg +3 -0
  16. package/client/assets/dropdown/icon-clear.svg +4 -0
  17. package/client/assets/dropdown/icon-enter.svg +3 -0
  18. package/client/components/icons/UtilityChangeType.tsx +21 -0
  19. package/client/components/ui/alert-dialog-panel.tsx +246 -0
  20. package/client/components/ui/alert-v02.tsx +214 -0
  21. package/client/components/ui/app-bar.tsx +299 -0
  22. package/client/components/ui/avatar-identity.tsx +122 -0
  23. package/client/components/ui/banner.tsx +108 -0
  24. package/client/components/ui/bottom-navigation.tsx +223 -0
  25. package/client/components/ui/breadcrumb-trail.tsx +170 -0
  26. package/client/components/ui/button-v01.tsx +238 -0
  27. package/client/components/ui/button.tsx +2 -0
  28. package/client/components/ui/card.tsx +108 -26
  29. package/client/components/ui/code-badge.tsx +62 -0
  30. package/client/components/ui/date-picker.tsx +188 -0
  31. package/client/components/ui/dropdown.tsx +448 -0
  32. package/client/components/ui/empty-state.tsx +197 -0
  33. package/client/components/ui/file-upload.tsx +214 -0
  34. package/client/components/ui/footer.tsx +179 -0
  35. package/client/components/ui/layout.tsx +1182 -0
  36. package/client/components/ui/pagination-v01.tsx +157 -0
  37. package/client/components/ui/password-field-v01.tsx +67 -0
  38. package/client/components/ui/search-field-v01.tsx +192 -0
  39. package/client/components/ui/section-heading.tsx +49 -0
  40. package/client/components/ui/sidebar.tsx +2 -3
  41. package/client/components/ui/standard-navigation.tsx +8 -2
  42. package/client/components/ui/stepper.tsx +148 -0
  43. package/client/components/ui/switch-v01.tsx +71 -0
  44. package/client/components/ui/system-bar.tsx +119 -0
  45. package/client/components/ui/table-v01.tsx +230 -0
  46. package/client/components/ui/tabs-v01.tsx +99 -0
  47. package/client/components/ui/tag-v02.tsx +113 -0
  48. package/client/components/ui/text-area-v01.tsx +33 -0
  49. package/client/components/ui/text-field-v01.tsx +108 -0
  50. package/client/components/ui/timeline.tsx +181 -0
  51. package/client/components/ui/toast-v01.tsx +206 -0
  52. package/client/components/ui/tooltip.tsx +264 -71
  53. package/client/global.css +9565 -4378
  54. package/client/storybook.css +125 -0
  55. package/client/ui/Accordion.stories.tsx +1 -1
  56. package/client/ui/AlertDialogPanel/AlertDialogPanel.stories.tsx +150 -0
  57. package/client/ui/AlertV02/AlertV02.stories.tsx +188 -0
  58. package/client/ui/AppBar/app-bar.stories.tsx +280 -0
  59. package/client/ui/AspectRatio.stories.tsx +1 -1
  60. package/client/ui/AvatarIdentity/AvatarIdentity.stories.tsx +77 -0
  61. package/client/ui/Banner/banner.stories.tsx +238 -0
  62. package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
  63. package/client/ui/BreadcrumbTrail/BreadcrumbTrail.stories.tsx +232 -0
  64. package/client/ui/Button.stories.tsx +1 -1
  65. package/client/ui/ButtonV01/ButtonV01.stories.tsx +417 -0
  66. package/client/ui/Calendar/Calendar.stories.tsx +1 -1
  67. package/client/ui/Card.stories.tsx +285 -168
  68. package/client/ui/Carousel/Carousel.stories.tsx +1 -1
  69. package/client/ui/Chart/Chart.stories.tsx +1 -1
  70. package/client/ui/Checkbox.stories.tsx +1 -1
  71. package/client/ui/Collapsible/Collapsible.stories.tsx +1 -1
  72. package/client/ui/Colors.stories.tsx +1 -1
  73. package/client/ui/Command/Command.stories.tsx +1 -1
  74. package/client/ui/ContextMenu/ContextMenu.stories.tsx +1 -1
  75. package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
  76. package/client/ui/Dialog.stories.tsx +1 -1
  77. package/client/ui/Drawer/Drawer.stories.tsx +1 -1
  78. package/client/ui/Dropdown/Dropdown.stories.tsx +205 -0
  79. package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
  80. package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
  81. package/client/ui/Footer/footer.stories.tsx +194 -0
  82. package/client/ui/Form/Form.stories.tsx +1 -1
  83. package/client/ui/Hero.stories.tsx +1 -1
  84. package/client/ui/HoverCard/HoverCard.stories.tsx +1 -1
  85. package/client/ui/Icons.stories.tsx +1 -1
  86. package/client/ui/Input.stories.tsx +1 -1
  87. package/client/ui/InputOTP/InputOTP.stories.tsx +1 -1
  88. package/client/ui/Label.stories.tsx +1 -1
  89. package/client/ui/Layout/Center.stories.tsx +100 -0
  90. package/client/ui/Layout/Columns.stories.tsx +136 -0
  91. package/client/ui/Layout/Container.stories.tsx +104 -0
  92. package/client/ui/Layout/FormLayout.stories.tsx +144 -0
  93. package/client/ui/Layout/Grid.stories.tsx +158 -0
  94. package/client/ui/Layout/GridSpan.stories.tsx +75 -0
  95. package/client/ui/Layout/HStack.stories.tsx +128 -0
  96. package/client/ui/Layout/Layout.stories.tsx +287 -0
  97. package/client/ui/Layout/LayoutContent.stories.tsx +91 -0
  98. package/client/ui/Layout/LayoutFooter.stories.tsx +77 -0
  99. package/client/ui/Layout/LayoutHeader.stories.tsx +104 -0
  100. package/client/ui/Layout/LayoutPanel.stories.tsx +143 -0
  101. package/client/ui/Layout/Positioned.stories.tsx +113 -0
  102. package/client/ui/Layout/Section.stories.tsx +84 -0
  103. package/client/ui/Layout/ShowHide.stories.tsx +99 -0
  104. package/client/ui/Layout/Spacer.stories.tsx +149 -0
  105. package/client/ui/Layout/Stack.stories.tsx +69 -0
  106. package/client/ui/Layout/StackItem.stories.tsx +124 -0
  107. package/client/ui/Layout/VStack.stories.tsx +101 -0
  108. package/client/ui/Layout/controls.ts +33 -0
  109. package/client/ui/Layout/demo.tsx +37 -0
  110. package/client/ui/Menubar/Menubar.stories.tsx +1 -1
  111. package/client/ui/MobileNav.stories.tsx +1 -1
  112. package/client/ui/Navigation.stories.tsx +1 -1
  113. package/client/ui/NavigationMenu/NavigationMenu.stories.tsx +1 -1
  114. package/client/ui/PageHeader.stories.tsx +13 -13
  115. package/client/ui/Pagination/Pagination.stories.tsx +178 -98
  116. package/client/ui/Pagination/PaginationLegacy.stories.tsx +121 -0
  117. package/client/ui/Panel.stories.tsx +1 -1
  118. package/client/ui/Popover/Popover.stories.tsx +1 -1
  119. package/client/ui/Progress/Progress.stories.tsx +1 -1
  120. package/client/ui/RadioGroup/RadioGroup.stories.tsx +1 -1
  121. package/client/ui/Resizable/Resizable.stories.tsx +1 -1
  122. package/client/ui/ScrollArea/ScrollArea.stories.tsx +1 -1
  123. package/client/ui/SearchField/SearchField.stories.tsx +153 -0
  124. package/client/ui/{SearchField.stories.tsx → SearchField/SearchFieldLegacy.stories.tsx} +2 -2
  125. package/client/ui/SectionBadge.stories.tsx +1 -1
  126. package/client/ui/Select.stories.tsx +1 -1
  127. package/client/ui/Separator/Separator.stories.tsx +1 -1
  128. package/client/ui/Sheet/Sheet.stories.tsx +1 -1
  129. package/client/ui/Sidebar/Sidebar.stories.tsx +57 -8
  130. package/client/ui/Skeleton/Skeleton.stories.tsx +1 -1
  131. package/client/ui/Slider/Slider.stories.tsx +1 -1
  132. package/client/ui/StandardNavigation.stories.tsx +1 -1
  133. package/client/ui/Stepper/Stepper.stories.tsx +344 -0
  134. package/client/ui/Switch.stories.tsx +1 -1
  135. package/client/ui/SwitchV01/SwitchV01.stories.tsx +152 -0
  136. package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
  137. package/client/ui/Table/TableV01.stories.tsx +256 -0
  138. package/client/ui/Tabs.stories.tsx +1 -1
  139. package/client/ui/TabsV01/TabsV01.stories.tsx +144 -0
  140. package/client/ui/Tag.stories.tsx +1 -1
  141. package/client/ui/TagV02/TagStatusV02.stories.tsx +132 -0
  142. package/client/ui/TagV02/TagV02.stories.tsx +142 -0
  143. package/client/ui/TextAreaV01/TextAreaV01.stories.tsx +146 -0
  144. package/client/ui/TextFieldV01/PasswordFieldV01.stories.tsx +161 -0
  145. package/client/ui/TextFieldV01/TextFieldV01.stories.tsx +160 -0
  146. package/client/ui/Textarea.stories.tsx +1 -1
  147. package/client/ui/Timeline/timeline.stories.tsx +404 -0
  148. package/client/ui/Toast/Toast.stories.tsx +1 -1
  149. package/client/ui/ToastV01/ToastV01.stories.tsx +201 -0
  150. package/client/ui/Toaster/Toaster.stories.tsx +1 -1
  151. package/client/ui/Toggle.stories.tsx +1 -1
  152. package/client/ui/Tooltip.stories.tsx +287 -114
  153. package/client/ui/Typography.stories.tsx +1 -1
  154. package/dist/index.cjs.js +1 -1
  155. package/dist/index.d.ts +649 -13
  156. package/dist/index.es.js +4405 -653
  157. package/package.json +1 -1
  158. package/client/components/ui/badge.tsx +0 -33
  159. package/client/components/ui/sonner.tsx +0 -26
  160. package/client/components/ui/toggle-group.tsx +0 -76
  161. package/client/ui/Alert/Alert.stories.tsx +0 -82
  162. package/client/ui/AlertDialog/AlertDialog.stories.tsx +0 -106
  163. package/client/ui/Avatar/Avatar.stories.tsx +0 -94
  164. package/client/ui/Badge/Badge.stories.tsx +0 -60
  165. package/client/ui/Breadcrumb/Breadcrumb.stories.tsx +0 -97
  166. package/client/ui/DropdownMenu/DropdownMenu.stories.tsx +0 -139
  167. package/client/ui/Sonner/Sonner.stories.tsx +0 -48
  168. package/client/ui/Table/Table.stories.tsx +0 -135
  169. package/client/ui/ToggleGroup/ToggleGroup.stories.tsx +0 -88
@@ -0,0 +1,448 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import iconCarrotExpand from "@/assets/dropdown/icon-carrot-expand.svg";
5
+ import iconCarrotContract from "@/assets/dropdown/icon-carrot-contract.svg";
6
+ import iconEnter from "@/assets/dropdown/icon-enter.svg";
7
+ import iconClear from "@/assets/dropdown/icon-clear.svg";
8
+ import { UtilityChangeType } from "@/components/icons/UtilityChangeType";
9
+
10
+ export type DropdownOption = {
11
+ value: string;
12
+ label: string;
13
+ secondary?: string;
14
+ actionLabel?: string;
15
+ };
16
+
17
+ /** Figma `icon_utility_change-type_*`. */
18
+ export type DropdownTagChangeType = "mild" | "medium" | "hot";
19
+
20
+ /** Figma `tags / tag`. */
21
+ export type DropdownTag = {
22
+ id: string;
23
+ label: string;
24
+ /** Renders `dot_branded_large`. */
25
+ dot?: boolean;
26
+ /** Renders one `icon_utility_change-type_*` glyph. */
27
+ changeType?: DropdownTagChangeType;
28
+ };
29
+
30
+ /** Two-tone glyph, so it is inline rather than a masked `<img>`. */
31
+ function ChangeTypeIcon({ kind }: { kind: DropdownTagChangeType }) {
32
+ return (
33
+ <UtilityChangeType
34
+ className={`dropdown-tag-change dropdown-tag-change--${kind}`}
35
+ />
36
+ );
37
+ }
38
+
39
+ export type DropdownProps = Omit<
40
+ React.HTMLAttributes<HTMLDivElement>,
41
+ "onChange"
42
+ > & {
43
+ label?: string;
44
+ placeholder?: string;
45
+ options?: DropdownOption[];
46
+ value?: string;
47
+ defaultValue?: string;
48
+ onValueChange?: (value: string) => void;
49
+ error?: boolean;
50
+ errorMessage?: string;
51
+ disabled?: boolean;
52
+ /** Force the menu open — used by Storybook state specimens. */
53
+ open?: boolean;
54
+ defaultOpen?: boolean;
55
+ onOpenChange?: (open: boolean) => void;
56
+ /** Seed the input text — used for typeahead / auto-suggest specimens. */
57
+ defaultInputValue?: string;
58
+ /** Figma `tags` row. Rendered between the label and the field. */
59
+ tags?: DropdownTag[];
60
+ /** Remove one tag. Omit to render tags as non-removable. */
61
+ onTagRemove?: (id: string) => void;
62
+ /** Clear-all control in the label row. Omit to hide it. */
63
+ onClearTags?: () => void;
64
+ name?: string;
65
+ id?: string;
66
+ };
67
+
68
+ function MaskIcon({ src, className }: { src: string; className?: string }) {
69
+ return (
70
+ <span
71
+ className={cn("dropdown-mask-icon", className)}
72
+ style={{
73
+ WebkitMaskImage: `url(${src})`,
74
+ maskImage: `url(${src})`,
75
+ }}
76
+ aria-hidden
77
+ />
78
+ );
79
+ }
80
+
81
+ function matchPrefix(query: string, label: string) {
82
+ if (!query) return null;
83
+ if (label.toLowerCase().startsWith(query.toLowerCase())) {
84
+ return {
85
+ matched: label.slice(0, query.length),
86
+ rest: label.slice(query.length),
87
+ };
88
+ }
89
+ return null;
90
+ }
91
+
92
+ const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(
93
+ (
94
+ {
95
+ className,
96
+ label = "Label",
97
+ placeholder = "Placeholder",
98
+ options = [],
99
+ value: valueProp,
100
+ defaultValue,
101
+ onValueChange,
102
+ error = false,
103
+ errorMessage = "Choose an option",
104
+ disabled = false,
105
+ open: openProp,
106
+ defaultOpen = false,
107
+ onOpenChange,
108
+ defaultInputValue,
109
+ tags,
110
+ onTagRemove,
111
+ onClearTags,
112
+ name,
113
+ id,
114
+ ...props
115
+ },
116
+ ref,
117
+ ) => {
118
+ const listId = React.useId();
119
+ const inputId = id ?? listId;
120
+ const rootRef = React.useRef<HTMLDivElement>(null);
121
+ const inputRef = React.useRef<HTMLInputElement>(null);
122
+
123
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
124
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(
125
+ defaultValue ?? "",
126
+ );
127
+ const [query, setQuery] = React.useState(defaultInputValue ?? "");
128
+ const [highlighted, setHighlighted] = React.useState(-1);
129
+ const [typing, setTyping] = React.useState(Boolean(defaultInputValue));
130
+
131
+ const isOpen = openProp ?? uncontrolledOpen;
132
+ const selectedValue = valueProp ?? uncontrolledValue;
133
+ const selected = options.find((option) => option.value === selectedValue);
134
+
135
+ const setOpen = (next: boolean) => {
136
+ if (openProp === undefined) setUncontrolledOpen(next);
137
+ onOpenChange?.(next);
138
+ if (!next) {
139
+ setTyping(false);
140
+ setQuery("");
141
+ }
142
+ };
143
+
144
+ const setValue = (next: string) => {
145
+ if (valueProp === undefined) setUncontrolledValue(next);
146
+ onValueChange?.(next);
147
+ };
148
+
149
+ const filtered = React.useMemo(() => {
150
+ if (!query) return options;
151
+ const needle = query.toLowerCase();
152
+ return options.filter(
153
+ (option) =>
154
+ option.label.toLowerCase().includes(needle) ||
155
+ option.secondary?.toLowerCase().includes(needle),
156
+ );
157
+ }, [options, query]);
158
+
159
+ const ghost = React.useMemo(() => {
160
+ if (!query || !isOpen) return "";
161
+ const hit = filtered.find(
162
+ (option) =>
163
+ option.label.toLowerCase().startsWith(query.toLowerCase()) &&
164
+ option.label.length > query.length,
165
+ );
166
+ if (!hit) return "";
167
+ return hit.label.slice(query.length);
168
+ }, [filtered, isOpen, query]);
169
+
170
+ React.useEffect(() => {
171
+ setHighlighted(query ? 0 : -1);
172
+ }, [query, isOpen]);
173
+
174
+ React.useEffect(() => {
175
+ const onPointerDown = (event: PointerEvent) => {
176
+ if (!rootRef.current?.contains(event.target as Node)) {
177
+ if (openProp === undefined) setUncontrolledOpen(false);
178
+ onOpenChange?.(false);
179
+ setTyping(Boolean(defaultInputValue));
180
+ setQuery(defaultInputValue ?? "");
181
+ }
182
+ };
183
+ document.addEventListener("pointerdown", onPointerDown);
184
+ return () => document.removeEventListener("pointerdown", onPointerDown);
185
+ }, [defaultInputValue, onOpenChange, openProp]);
186
+
187
+ const selectOption = (option: DropdownOption) => {
188
+ setValue(option.value);
189
+ setQuery("");
190
+ setTyping(false);
191
+ setOpen(false);
192
+ };
193
+
194
+ const clearValue = (event: React.MouseEvent) => {
195
+ event.preventDefault();
196
+ event.stopPropagation();
197
+ setValue("");
198
+ setQuery("");
199
+ setTyping(false);
200
+ inputRef.current?.focus();
201
+ setOpen(true);
202
+ };
203
+
204
+ const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
205
+ if (event.key === "ArrowDown") {
206
+ event.preventDefault();
207
+ if (!isOpen) setOpen(true);
208
+ setHighlighted((index) =>
209
+ Math.min(index + 1, Math.max(filtered.length - 1, 0)),
210
+ );
211
+ } else if (event.key === "ArrowUp") {
212
+ event.preventDefault();
213
+ setHighlighted((index) => Math.max(index - 1, 0));
214
+ } else if (event.key === "Enter") {
215
+ event.preventDefault();
216
+ const option = filtered[highlighted];
217
+ if (option) selectOption(option);
218
+ } else if (event.key === "Escape") {
219
+ setOpen(false);
220
+ inputRef.current?.blur();
221
+ }
222
+ };
223
+
224
+ const displayValue = isOpen ? query : (selected?.label ?? query);
225
+ const showPlaceholder = !displayValue && !ghost;
226
+ const isRich = filtered.some(
227
+ (option) => option.secondary || option.actionLabel,
228
+ );
229
+ const trailingIcon = error
230
+ ? "expand"
231
+ : isOpen && (typing || query)
232
+ ? "enter"
233
+ : isOpen
234
+ ? "contract"
235
+ : selected
236
+ ? "clear"
237
+ : "expand";
238
+
239
+ return (
240
+ <div
241
+ ref={(node) => {
242
+ (rootRef as React.MutableRefObject<HTMLDivElement | null>).current =
243
+ node;
244
+ if (typeof ref === "function") ref(node);
245
+ else if (ref) ref.current = node;
246
+ }}
247
+ className={cn("dropdown font-mazzardSoft", className)}
248
+ data-open={isOpen || undefined}
249
+ data-filled={(!showPlaceholder && !isOpen) || undefined}
250
+ data-error={error || undefined}
251
+ data-disabled={disabled || undefined}
252
+ {...props}
253
+ >
254
+ {label ? (
255
+ <div className="dropdown-label-row">
256
+ <label className="dropdown-label overline-medium" htmlFor={inputId}>
257
+ {label}
258
+ </label>
259
+ {onClearTags ? (
260
+ <span className="dropdown-label-actions">
261
+ <button
262
+ type="button"
263
+ className="dropdown-label-clear"
264
+ aria-label="Clear all"
265
+ onClick={onClearTags}
266
+ >
267
+ <MaskIcon
268
+ src={iconClear}
269
+ className="dropdown-label-clear-icon"
270
+ />
271
+ </button>
272
+ </span>
273
+ ) : null}
274
+ </div>
275
+ ) : null}
276
+
277
+ {tags && tags.length > 0 ? (
278
+ <ul className="dropdown-tags">
279
+ {tags.map((tag) => (
280
+ <li key={tag.id} className="dropdown-tag">
281
+ <span className="dropdown-tag-label">{tag.label}</span>
282
+ {tag.dot ? (
283
+ <span className="dropdown-tag-dot" aria-hidden />
284
+ ) : null}
285
+ {tag.changeType ? (
286
+ <ChangeTypeIcon kind={tag.changeType} />
287
+ ) : null}
288
+ {onTagRemove ? (
289
+ <button
290
+ type="button"
291
+ className="dropdown-tag-remove"
292
+ aria-label={`Remove ${tag.label}`}
293
+ onClick={() => onTagRemove(tag.id)}
294
+ >
295
+ <MaskIcon
296
+ src={iconClear}
297
+ className="dropdown-tag-remove-icon"
298
+ />
299
+ </button>
300
+ ) : null}
301
+ </li>
302
+ ))}
303
+ </ul>
304
+ ) : null}
305
+
306
+ <div className="dropdown-field">
307
+ <div
308
+ className="dropdown-input"
309
+ onClick={() => {
310
+ if (disabled) return;
311
+ inputRef.current?.focus();
312
+ setOpen(true);
313
+ }}
314
+ >
315
+ <div className="dropdown-input-value">
316
+ {showPlaceholder ? (
317
+ <span className="dropdown-placeholder">{placeholder}</span>
318
+ ) : (
319
+ <span className="dropdown-entry">
320
+ {displayValue}
321
+ {ghost ? (
322
+ <span className="dropdown-ghost">{ghost}</span>
323
+ ) : null}
324
+ </span>
325
+ )}
326
+ </div>
327
+ <input
328
+ ref={inputRef}
329
+ id={inputId}
330
+ name={name}
331
+ className="dropdown-native"
332
+ value={displayValue}
333
+ disabled={disabled}
334
+ autoComplete="off"
335
+ role="combobox"
336
+ aria-expanded={isOpen}
337
+ aria-controls={listId}
338
+ aria-invalid={error || undefined}
339
+ aria-autocomplete="list"
340
+ placeholder=""
341
+ onFocus={() => {
342
+ if (disabled) return;
343
+ setOpen(true);
344
+ if (selected && !query) setQuery(selected.label);
345
+ }}
346
+ onChange={(event) => {
347
+ setTyping(true);
348
+ setQuery(event.target.value);
349
+ if (!isOpen) setOpen(true);
350
+ if (selected && event.target.value !== selected.label) {
351
+ setValue("");
352
+ }
353
+ }}
354
+ onKeyDown={onKeyDown}
355
+ />
356
+ <div className="dropdown-actions">
357
+ {trailingIcon === "clear" ? (
358
+ <button
359
+ type="button"
360
+ className="dropdown-icon-button"
361
+ aria-label="Clear"
362
+ onClick={clearValue}
363
+ >
364
+ <MaskIcon src={iconClear} className="dropdown-icon-clear" />
365
+ </button>
366
+ ) : (
367
+ <MaskIcon
368
+ src={
369
+ trailingIcon === "enter"
370
+ ? iconEnter
371
+ : trailingIcon === "contract"
372
+ ? iconCarrotContract
373
+ : iconCarrotExpand
374
+ }
375
+ className={
376
+ trailingIcon === "enter"
377
+ ? "dropdown-icon-enter"
378
+ : "dropdown-icon-carrot"
379
+ }
380
+ />
381
+ )}
382
+ </div>
383
+ </div>
384
+
385
+ {isOpen && !error ? (
386
+ <ul
387
+ id={listId}
388
+ role="listbox"
389
+ className={cn("dropdown-menu", isRich && "dropdown-menu--rich")}
390
+ >
391
+ {filtered.map((option, index) => {
392
+ const prefix = query ? matchPrefix(query, option.label) : null;
393
+ const isHighlighted = index === highlighted;
394
+ return (
395
+ <li key={option.value} role="presentation">
396
+ <button
397
+ type="button"
398
+ role="option"
399
+ aria-selected={option.value === selectedValue}
400
+ className={cn(
401
+ "dropdown-item",
402
+ (option.secondary || option.actionLabel) &&
403
+ "dropdown-item--rich",
404
+ )}
405
+ data-highlighted={isHighlighted || undefined}
406
+ onMouseEnter={() => setHighlighted(index)}
407
+ onMouseDown={(event) => event.preventDefault()}
408
+ onClick={() => selectOption(option)}
409
+ >
410
+ <span className="dropdown-item-primary">
411
+ {prefix ? (
412
+ <>
413
+ <span className="dropdown-item-match">
414
+ {prefix.matched}
415
+ </span>
416
+ <span className="dropdown-item-rest">
417
+ {prefix.rest}
418
+ </span>
419
+ </>
420
+ ) : (
421
+ option.label
422
+ )}
423
+ </span>
424
+ {option.actionLabel ? (
425
+ <span className="dropdown-item-action">
426
+ {option.actionLabel}
427
+ </span>
428
+ ) : option.secondary ? (
429
+ <span className="dropdown-item-secondary">
430
+ {option.secondary}
431
+ </span>
432
+ ) : null}
433
+ </button>
434
+ </li>
435
+ );
436
+ })}
437
+ </ul>
438
+ ) : null}
439
+ </div>
440
+
441
+ {error ? <p className="dropdown-error">{errorMessage}</p> : null}
442
+ </div>
443
+ );
444
+ },
445
+ );
446
+ Dropdown.displayName = "Dropdown";
447
+
448
+ export { Dropdown };
@@ -0,0 +1,197 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // ─── EmptyState Root ──────────────────────────────────────────────────────────
5
+
6
+ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /**
8
+ * Large display headline (e.g. "Whoops, 404" or "No Messages Yet").
9
+ * Rendered above the title. Ignored when `headline` slot is used.
10
+ */
11
+ headline?: string;
12
+ /**
13
+ * Main title below the headline.
14
+ * Ignored when `title` slot is used.
15
+ */
16
+ title?: string;
17
+ /**
18
+ * Subtitle rendered between title and body text.
19
+ * Ignored when `subtitle` slot is used.
20
+ */
21
+ subtitle?: string;
22
+ /**
23
+ * Body copy below the title.
24
+ * Ignored when `text` slot is used.
25
+ */
26
+ text?: string;
27
+ /**
28
+ * URL for an illustration / image rendered above the headline.
29
+ * Ignored when `media` slot is used.
30
+ */
31
+ image?: string;
32
+ /** Alt text for the image */
33
+ imageAlt?: string;
34
+ /**
35
+ * Icon rendered in the media area when no `image` is provided.
36
+ * Pass any React node (SVG, lucide icon, etc.).
37
+ * Ignored when `media` slot is used.
38
+ */
39
+ icon?: React.ReactNode;
40
+ /**
41
+ * Label for the primary action button.
42
+ * When provided an `<EmptyStateAction>` button is rendered automatically.
43
+ * Ignored when `actions` slot is used.
44
+ */
45
+ actionText?: string;
46
+ /** Fires when the auto-rendered action button is clicked */
47
+ onClickAction?: () => void;
48
+
49
+ // ── Slot overrides ──────────────────────────────────────────────────────
50
+ /** Replaces the media area (image / icon) */
51
+ mediaSlot?: React.ReactNode;
52
+ /** Replaces the headline */
53
+ headlineSlot?: React.ReactNode;
54
+ /** Replaces the title */
55
+ titleSlot?: React.ReactNode;
56
+ /** Replaces the subtitle */
57
+ subtitleSlot?: React.ReactNode;
58
+ /** Replaces the body text */
59
+ textSlot?: React.ReactNode;
60
+ /** Replaces the actions area */
61
+ actionsSlot?: React.ReactNode;
62
+ }
63
+
64
+ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
65
+ (
66
+ {
67
+ className,
68
+ children,
69
+ headline,
70
+ title,
71
+ subtitle,
72
+ text,
73
+ image,
74
+ imageAlt = "",
75
+ icon,
76
+ actionText,
77
+ onClickAction,
78
+ mediaSlot,
79
+ headlineSlot,
80
+ titleSlot,
81
+ subtitleSlot,
82
+ textSlot,
83
+ actionsSlot,
84
+ ...props
85
+ },
86
+ ref
87
+ ) => {
88
+ const hasMedia = mediaSlot || image || icon;
89
+ const hasHeadline = headlineSlot || headline;
90
+ const hasTitle = titleSlot || title;
91
+ const hasSubtitle = subtitleSlot || subtitle;
92
+ const hasText = textSlot || text;
93
+ const hasActions = actionsSlot || actionText;
94
+
95
+ return (
96
+ <div
97
+ ref={ref}
98
+ className={cn("empty-state", className)}
99
+ {...props}
100
+ >
101
+ {/* Media: image or icon */}
102
+ {hasMedia && (
103
+ <div className="empty-state-media">
104
+ {mediaSlot ?? (
105
+ image ? (
106
+ <img
107
+ src={image}
108
+ alt={imageAlt}
109
+ className="empty-state-image"
110
+ />
111
+ ) : (
112
+ <span className="empty-state-icon">{icon}</span>
113
+ )
114
+ )}
115
+ </div>
116
+ )}
117
+
118
+ {/* Headline */}
119
+ {hasHeadline && (
120
+ <div className="empty-state-headline">
121
+ {headlineSlot ?? headline}
122
+ </div>
123
+ )}
124
+
125
+ {/* Title */}
126
+ {hasTitle && (
127
+ <div className="empty-state-title">
128
+ {titleSlot ?? title}
129
+ </div>
130
+ )}
131
+
132
+ {/* Subtitle */}
133
+ {hasSubtitle && (
134
+ <div className="empty-state-subtitle">
135
+ {subtitleSlot ?? subtitle}
136
+ </div>
137
+ )}
138
+
139
+ {/* Body text */}
140
+ {hasText && (
141
+ <div className="empty-state-text">
142
+ {textSlot ?? text}
143
+ </div>
144
+ )}
145
+
146
+ {/* Default slot (custom content — e.g. a card grid) */}
147
+ {children && (
148
+ <div className="empty-state-default">{children}</div>
149
+ )}
150
+
151
+ {/* Actions */}
152
+ {hasActions && (
153
+ <div className="empty-state-actions">
154
+ {actionsSlot ?? (
155
+ <EmptyStateAction onClick={onClickAction}>
156
+ {actionText}
157
+ </EmptyStateAction>
158
+ )}
159
+ </div>
160
+ )}
161
+ </div>
162
+ );
163
+ }
164
+ );
165
+ EmptyState.displayName = "EmptyState";
166
+
167
+ // ─── EmptyStateAction ─────────────────────────────────────────────────────────
168
+ // Pre-styled CTA button. Use inside `actionsSlot` for custom actions,
169
+ // or let `EmptyState` auto-render one via the `actionText` prop.
170
+
171
+ export interface EmptyStateActionProps
172
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
173
+ /** Visual variant */
174
+ variant?: "default" | "primary" | "outline";
175
+ }
176
+
177
+ const EmptyStateAction = React.forwardRef<
178
+ HTMLButtonElement,
179
+ EmptyStateActionProps
180
+ >(({ className, variant = "default", ...props }, ref) => (
181
+ <button
182
+ ref={ref}
183
+ type="button"
184
+ className={cn(
185
+ "empty-state-action-btn",
186
+ variant === "primary" && "empty-state-action-btn--primary",
187
+ variant === "outline" && "empty-state-action-btn--outline",
188
+ className
189
+ )}
190
+ {...props}
191
+ />
192
+ ));
193
+ EmptyStateAction.displayName = "EmptyStateAction";
194
+
195
+ // ─── Exports ─────────────────────────────────────────────────────────────────
196
+
197
+ export { EmptyState, EmptyStateAction };