keystone-design-bootstrap 1.0.3

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 (182) hide show
  1. package/README.md +179 -0
  2. package/package.json +59 -0
  3. package/src/contexts/ThemeContext.tsx +34 -0
  4. package/src/contexts/index.ts +1 -0
  5. package/src/design_system/elements/IconComponent.tsx +98 -0
  6. package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
  7. package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
  8. package/src/design_system/elements/avatar/avatar.tsx +131 -0
  9. package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
  10. package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
  11. package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
  12. package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
  13. package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
  14. package/src/design_system/elements/avatar/utils.ts +12 -0
  15. package/src/design_system/elements/badges/avatar.tsx +132 -0
  16. package/src/design_system/elements/badges/badge-groups.tsx +176 -0
  17. package/src/design_system/elements/badges/badge-types.ts +266 -0
  18. package/src/design_system/elements/badges/badges.tsx +430 -0
  19. package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
  20. package/src/design_system/elements/button-group/button-group.tsx +106 -0
  21. package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
  22. package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
  23. package/src/design_system/elements/buttons/button-utility.tsx +116 -0
  24. package/src/design_system/elements/buttons/button.aman.tsx +174 -0
  25. package/src/design_system/elements/buttons/button.tsx +271 -0
  26. package/src/design_system/elements/buttons/close-button.tsx +42 -0
  27. package/src/design_system/elements/buttons/round-button.tsx +29 -0
  28. package/src/design_system/elements/buttons/social-button.tsx +148 -0
  29. package/src/design_system/elements/buttons/social-logos.tsx +115 -0
  30. package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
  31. package/src/design_system/elements/carousel/carousel.tsx +308 -0
  32. package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
  33. package/src/design_system/elements/date-picker/calendar.tsx +101 -0
  34. package/src/design_system/elements/date-picker/cell.tsx +106 -0
  35. package/src/design_system/elements/date-picker/date-input.tsx +32 -0
  36. package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
  37. package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
  38. package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
  39. package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
  40. package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
  41. package/src/design_system/elements/form/form.tsx +10 -0
  42. package/src/design_system/elements/form/hook-form.tsx +75 -0
  43. package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
  44. package/src/design_system/elements/index.tsx +158 -0
  45. package/src/design_system/elements/input/hint-text.tsx +33 -0
  46. package/src/design_system/elements/input/input-group.tsx +133 -0
  47. package/src/design_system/elements/input/input.aman.tsx +172 -0
  48. package/src/design_system/elements/input/input.tsx +271 -0
  49. package/src/design_system/elements/input/label.tsx +50 -0
  50. package/src/design_system/elements/label/label.tsx +50 -0
  51. package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
  52. package/src/design_system/elements/map/GoogleMap.tsx +286 -0
  53. package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
  54. package/src/design_system/elements/modals/modal.tsx +41 -0
  55. package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
  56. package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
  57. package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
  58. package/src/design_system/elements/pagination/pagination.tsx +330 -0
  59. package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
  60. package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
  61. package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
  62. package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
  63. package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
  64. package/src/design_system/elements/rating/rating-badge.tsx +144 -0
  65. package/src/design_system/elements/rating/rating-stars.tsx +77 -0
  66. package/src/design_system/elements/select/combobox.tsx +152 -0
  67. package/src/design_system/elements/select/multi-select.tsx +363 -0
  68. package/src/design_system/elements/select/popover.tsx +34 -0
  69. package/src/design_system/elements/select/select-item.tsx +97 -0
  70. package/src/design_system/elements/select/select-native.tsx +69 -0
  71. package/src/design_system/elements/select/select.aman.tsx +75 -0
  72. package/src/design_system/elements/select/select.tsx +146 -0
  73. package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
  74. package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
  75. package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
  76. package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
  77. package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
  78. package/src/design_system/elements/tabs/tabs.tsx +225 -0
  79. package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
  80. package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
  81. package/src/design_system/elements/tags/tags.tsx +176 -0
  82. package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
  83. package/src/design_system/elements/textarea/textarea.tsx +111 -0
  84. package/src/design_system/elements/toggle/toggle.tsx +140 -0
  85. package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
  86. package/src/design_system/hooks/use-breakpoint.ts +37 -0
  87. package/src/design_system/hooks/use-resize-observer.ts +68 -0
  88. package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
  89. package/src/design_system/logo/keystone-logo.tsx +22 -0
  90. package/src/design_system/sections/about-home.aman.tsx +85 -0
  91. package/src/design_system/sections/about-home.tsx +115 -0
  92. package/src/design_system/sections/blog-cards.tsx +848 -0
  93. package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
  94. package/src/design_system/sections/blog-gallery.tsx +204 -0
  95. package/src/design_system/sections/blog-home.aman.tsx +84 -0
  96. package/src/design_system/sections/blog-home.tsx +153 -0
  97. package/src/design_system/sections/blog-post.aman.tsx +74 -0
  98. package/src/design_system/sections/blog-post.tsx +301 -0
  99. package/src/design_system/sections/blog-section.aman.tsx +101 -0
  100. package/src/design_system/sections/blog-section.tsx +179 -0
  101. package/src/design_system/sections/contact-home.tsx +25 -0
  102. package/src/design_system/sections/contact-section.aman.tsx +173 -0
  103. package/src/design_system/sections/contact-section.tsx +143 -0
  104. package/src/design_system/sections/faq-grid.aman.tsx +79 -0
  105. package/src/design_system/sections/faq-grid.tsx +102 -0
  106. package/src/design_system/sections/faq-home.aman.tsx +92 -0
  107. package/src/design_system/sections/faq-home.tsx +134 -0
  108. package/src/design_system/sections/feature-tab.tsx +43 -0
  109. package/src/design_system/sections/feature-text.tsx +284 -0
  110. package/src/design_system/sections/footer-home.aman.tsx +62 -0
  111. package/src/design_system/sections/footer-home.tsx +259 -0
  112. package/src/design_system/sections/generic-header-component.tsx +103 -0
  113. package/src/design_system/sections/header-navigation.aman.tsx +360 -0
  114. package/src/design_system/sections/header-navigation.tsx +334 -0
  115. package/src/design_system/sections/hero-faq.aman.tsx +38 -0
  116. package/src/design_system/sections/hero-faq.tsx +55 -0
  117. package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
  118. package/src/design_system/sections/hero-generic-text.tsx +51 -0
  119. package/src/design_system/sections/hero-home.aman.tsx +84 -0
  120. package/src/design_system/sections/hero-home.tsx +246 -0
  121. package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
  122. package/src/design_system/sections/hero-location-detail.tsx +72 -0
  123. package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
  124. package/src/design_system/sections/hero-service-detail.tsx +51 -0
  125. package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
  126. package/src/design_system/sections/hero-social-media.tsx +35 -0
  127. package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
  128. package/src/design_system/sections/hero-testimonials.tsx +55 -0
  129. package/src/design_system/sections/home-hero-component.tsx +228 -0
  130. package/src/design_system/sections/index.tsx +131 -0
  131. package/src/design_system/sections/job-gallery.aman.tsx +91 -0
  132. package/src/design_system/sections/job-gallery.tsx +183 -0
  133. package/src/design_system/sections/location-details-section.aman.tsx +179 -0
  134. package/src/design_system/sections/location-details-section.tsx +196 -0
  135. package/src/design_system/sections/location-grid.aman.tsx +76 -0
  136. package/src/design_system/sections/location-grid.tsx +123 -0
  137. package/src/design_system/sections/services-grid.aman.tsx +85 -0
  138. package/src/design_system/sections/services-grid.tsx +104 -0
  139. package/src/design_system/sections/services-home.aman.tsx +78 -0
  140. package/src/design_system/sections/services-home.tsx +131 -0
  141. package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
  142. package/src/design_system/sections/social-media-grid.tsx +189 -0
  143. package/src/design_system/sections/statistics-section.aman.tsx +79 -0
  144. package/src/design_system/sections/statistics-section.tsx +97 -0
  145. package/src/design_system/sections/team-grid.aman.tsx +85 -0
  146. package/src/design_system/sections/team-grid.tsx +88 -0
  147. package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
  148. package/src/design_system/sections/testimonials-home.tsx +90 -0
  149. package/src/design_system/sections/values-section.aman.tsx +73 -0
  150. package/src/design_system/sections/values-section.tsx +128 -0
  151. package/src/design_system/utils/icon-mapping.tsx +28 -0
  152. package/src/index.ts +7 -0
  153. package/src/lib/component-registry.ts +53 -0
  154. package/src/lib/hooks/index.ts +8 -0
  155. package/src/lib/hooks/use-breakpoint.ts +37 -0
  156. package/src/lib/hooks/use-clipboard.ts +79 -0
  157. package/src/lib/hooks/use-resize-observer.ts +68 -0
  158. package/src/lib/server-api.ts +115 -0
  159. package/src/styles/style-overrides.aman.css +101 -0
  160. package/src/styles/theme.css +224 -0
  161. package/src/styles/typography.css +430 -0
  162. package/src/themes/index.ts +23 -0
  163. package/src/types/api/blog-post.ts +53 -0
  164. package/src/types/api/company-information.ts +44 -0
  165. package/src/types/api/contact.ts +63 -0
  166. package/src/types/api/faq.ts +37 -0
  167. package/src/types/api/job-posting.ts +34 -0
  168. package/src/types/api/location.ts +36 -0
  169. package/src/types/api/photos.ts +28 -0
  170. package/src/types/api/service.ts +37 -0
  171. package/src/types/api/social-post.ts +28 -0
  172. package/src/types/api/team-member.ts +29 -0
  173. package/src/types/api/testimonial.ts +29 -0
  174. package/src/types/api/website-photos.ts +22 -0
  175. package/src/types/config.ts +21 -0
  176. package/src/types/index.ts +21 -0
  177. package/src/utils/countries.tsx +1351 -0
  178. package/src/utils/cx.ts +25 -0
  179. package/src/utils/gradient-placeholder.ts +59 -0
  180. package/src/utils/is-react-component.ts +33 -0
  181. package/src/utils/markdown-toc.ts +54 -0
  182. package/src/utils/photo-helpers.ts +94 -0
@@ -0,0 +1,363 @@
1
+ "use client";
2
+
3
+ import type { FocusEventHandler, KeyboardEvent, PointerEventHandler, RefAttributes, RefObject } from "react";
4
+ import { createContext, useCallback, useContext, useRef, useState } from "react";
5
+ import { SearchLg } from "@untitledui/icons";
6
+ import { FocusScope, useFilter, useFocusManager } from "react-aria";
7
+ import type { ComboBoxProps as AriaComboBoxProps, GroupProps as AriaGroupProps, ListBoxProps as AriaListBoxProps, Key } from "react-aria-components";
8
+ import { ComboBox as AriaComboBox, Group as AriaGroup, Input as AriaInput, ListBox as AriaListBox, ComboBoxStateContext } from "react-aria-components";
9
+ import type { ListData } from "react-stately";
10
+ import { useListData } from "react-stately";
11
+ import { Avatar } from '../avatar/avatar';
12
+ import type { IconComponentType } from '../badges/badge-types';
13
+ import { HintText } from '../input/hint-text';
14
+ import { Label } from '../input/label';
15
+ import { Popover } from './popover';
16
+ import { type SelectItemType, sizes } from './select';
17
+ import { TagCloseX } from '../tags/base-components/tag-close-x';
18
+ import { useResizeObserver } from '../../../lib/hooks/use-resize-observer';
19
+ import { cx } from '../../../utils/cx';
20
+ import { SelectItem } from "./select-item";
21
+
22
+ interface ComboBoxValueProps extends AriaGroupProps {
23
+ size: "sm" | "md";
24
+ shortcut?: boolean;
25
+ isDisabled?: boolean;
26
+ placeholder?: string;
27
+ shortcutClassName?: string;
28
+ placeholderIcon?: IconComponentType | null;
29
+ ref?: RefObject<HTMLDivElement | null>;
30
+ onFocus?: FocusEventHandler;
31
+ onPointerEnter?: PointerEventHandler;
32
+ }
33
+
34
+ const ComboboxContext = createContext<{
35
+ size: "sm" | "md";
36
+ selectedKeys: Key[];
37
+ selectedItems: ListData<SelectItemType>;
38
+ onRemove: (keys: Set<Key>) => void;
39
+ onInputChange: (value: string) => void;
40
+ }>({
41
+ size: "sm",
42
+ selectedKeys: [],
43
+ selectedItems: {} as ListData<SelectItemType>,
44
+ onRemove: () => {},
45
+ onInputChange: () => {},
46
+ });
47
+
48
+ interface MultiSelectProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement> {
49
+ hint?: string;
50
+ label?: string;
51
+ tooltip?: string;
52
+ size?: "sm" | "md";
53
+ placeholder?: string;
54
+ shortcut?: boolean;
55
+ items?: SelectItemType[];
56
+ popoverClassName?: string;
57
+ shortcutClassName?: string;
58
+ selectedItems: ListData<SelectItemType>;
59
+ placeholderIcon?: IconComponentType | null;
60
+ children: AriaListBoxProps<SelectItemType>["children"];
61
+ onItemCleared?: (key: Key) => void;
62
+ onItemInserted?: (key: Key) => void;
63
+ }
64
+
65
+ export const MultiSelectBase = ({
66
+ items,
67
+ children,
68
+ size = "sm",
69
+ selectedItems,
70
+ onItemCleared,
71
+ onItemInserted,
72
+ shortcut,
73
+ placeholder = "Search",
74
+ // Omit these props to avoid conflicts with the `Select` component
75
+ name: _name,
76
+ className: _className,
77
+ ...props
78
+ }: MultiSelectProps) => {
79
+ const { contains } = useFilter({ sensitivity: "base" });
80
+ const selectedKeys = selectedItems.items.map((item) => item.id);
81
+
82
+ const filter = useCallback(
83
+ (item: SelectItemType, filterText: string) => {
84
+ return !selectedKeys.includes(item.id) && contains(item.label || item.supportingText || "", filterText);
85
+ },
86
+ [contains, selectedKeys],
87
+ );
88
+
89
+ const accessibleList = useListData({
90
+ initialItems: items,
91
+ filter,
92
+ });
93
+
94
+ const onRemove = useCallback(
95
+ (keys: Set<Key>) => {
96
+ const key = keys.values().next().value;
97
+
98
+ if (!key) return;
99
+
100
+ selectedItems.remove(key);
101
+ onItemCleared?.(key);
102
+ },
103
+ [selectedItems, onItemCleared],
104
+ );
105
+
106
+ const onSelectionChange = (id: Key | null) => {
107
+ if (!id) {
108
+ return;
109
+ }
110
+
111
+ const item = accessibleList.getItem(id);
112
+
113
+ if (!item) {
114
+ return;
115
+ }
116
+
117
+ if (!selectedKeys.includes(id as string)) {
118
+ selectedItems.append(item);
119
+ onItemInserted?.(id);
120
+ }
121
+
122
+ accessibleList.setFilterText("");
123
+ };
124
+
125
+ const onInputChange = (value: string) => {
126
+ accessibleList.setFilterText(value);
127
+ };
128
+
129
+ const placeholderRef = useRef<HTMLDivElement>(null);
130
+ const [popoverWidth, setPopoverWidth] = useState("");
131
+
132
+ // Resize observer for popover width
133
+ const onResize = useCallback(() => {
134
+ if (!placeholderRef.current) return;
135
+ let divRect = placeholderRef.current?.getBoundingClientRect();
136
+ setPopoverWidth(divRect.width + "px");
137
+ }, [placeholderRef, setPopoverWidth]);
138
+
139
+ useResizeObserver({
140
+ ref: placeholderRef,
141
+ onResize: onResize,
142
+ box: "border-box",
143
+ });
144
+
145
+ return (
146
+ <ComboboxContext.Provider
147
+ value={{
148
+ size,
149
+ selectedKeys,
150
+ selectedItems,
151
+ onInputChange,
152
+ onRemove,
153
+ }}
154
+ >
155
+ <AriaComboBox
156
+ allowsEmptyCollection
157
+ menuTrigger="focus"
158
+ items={accessibleList.items}
159
+ onInputChange={onInputChange}
160
+ inputValue={accessibleList.filterText}
161
+ // This keeps the combobox popover open and the input value unchanged when an item is selected.
162
+ selectedKey={null}
163
+ onSelectionChange={onSelectionChange}
164
+ {...props}
165
+ >
166
+ {(state) => (
167
+ <div className="flex flex-col gap-1.5">
168
+ {props.label && (
169
+ <Label isRequired={state.isRequired} tooltip={props.tooltip}>
170
+ {props.label}
171
+ </Label>
172
+ )}
173
+
174
+ <MultiSelectTagsValue
175
+ size={size}
176
+ shortcut={shortcut}
177
+ ref={placeholderRef}
178
+ placeholder={placeholder}
179
+ // This is a workaround to correctly calculating the trigger width
180
+ // while using ResizeObserver wasn't 100% reliable.
181
+ onFocus={onResize}
182
+ onPointerEnter={onResize}
183
+ />
184
+
185
+ <Popover size={"md"} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={props?.popoverClassName}>
186
+ <AriaListBox selectionMode="multiple" className="size-full outline-hidden">
187
+ {children}
188
+ </AriaListBox>
189
+ </Popover>
190
+
191
+ {props.hint && <HintText isInvalid={state.isInvalid}>{props.hint}</HintText>}
192
+ </div>
193
+ )}
194
+ </AriaComboBox>
195
+ </ComboboxContext.Provider>
196
+ );
197
+ };
198
+
199
+ const InnerMultiSelect = ({ isDisabled, shortcut, shortcutClassName, placeholder }: Omit<MultiSelectProps, "selectedItems" | "children">) => {
200
+ const focusManager = useFocusManager();
201
+ const comboBoxContext = useContext(ComboboxContext);
202
+ const comboBoxStateContext = useContext(ComboBoxStateContext);
203
+
204
+ const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
205
+ const isCaretAtStart = event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;
206
+
207
+ if (!isCaretAtStart && event.currentTarget.value !== "") {
208
+ return;
209
+ }
210
+
211
+ switch (event.key) {
212
+ case "Backspace":
213
+ case "ArrowLeft":
214
+ focusManager?.focusPrevious({ wrap: false, tabbable: false });
215
+ break;
216
+ case "ArrowRight":
217
+ focusManager?.focusNext({ wrap: false, tabbable: false });
218
+ break;
219
+ }
220
+ };
221
+
222
+ // Ensure dropdown opens on click even if input is already focused
223
+ const handleInputMouseDown = (_event: React.MouseEvent<HTMLInputElement>) => {
224
+ if (comboBoxStateContext && !comboBoxStateContext.isOpen) {
225
+ comboBoxStateContext.open();
226
+ }
227
+ };
228
+
229
+ const handleTagKeyDown = (event: KeyboardEvent<HTMLButtonElement>, value: Key) => {
230
+ // Do nothing when tab is clicked to move focus from the tag to the input element.
231
+ if (event.key === "Tab") {
232
+ return;
233
+ }
234
+
235
+ event.preventDefault();
236
+
237
+ const isFirstTag = comboBoxContext?.selectedItems?.items?.[0]?.id === value;
238
+
239
+ switch (event.key) {
240
+ case " ":
241
+ case "Enter":
242
+ case "Backspace":
243
+ if (isFirstTag) {
244
+ focusManager?.focusNext({ wrap: false, tabbable: false });
245
+ } else {
246
+ focusManager?.focusPrevious({ wrap: false, tabbable: false });
247
+ }
248
+
249
+ comboBoxContext.onRemove(new Set([value]));
250
+ break;
251
+
252
+ case "ArrowLeft":
253
+ focusManager?.focusPrevious({ wrap: false, tabbable: false });
254
+ break;
255
+ case "ArrowRight":
256
+ focusManager?.focusNext({ wrap: false, tabbable: false });
257
+ break;
258
+ case "Escape":
259
+ comboBoxStateContext?.close();
260
+ break;
261
+ }
262
+ };
263
+
264
+ const isSelectionEmpty = comboBoxContext?.selectedItems?.items?.length === 0;
265
+
266
+ return (
267
+ <div className="relative flex w-full flex-1 flex-row flex-wrap items-center justify-start gap-1.5">
268
+ {!isSelectionEmpty &&
269
+ comboBoxContext?.selectedItems?.items?.map((value) => (
270
+ <span key={value.id} className="flex items-center rounded-md bg-primary py-0.5 pr-1 pl-1.25 ring-1 ring-primary ring-inset">
271
+ <Avatar size="xxs" alt={value?.label} src={value?.avatarUrl} />
272
+
273
+ <p className="ml-1.25 truncate text-sm font-medium whitespace-nowrap text-secondary select-none">{value?.label}</p>
274
+
275
+ <TagCloseX
276
+ size="md"
277
+ isDisabled={isDisabled}
278
+ className="ml-0.75"
279
+ // For workaround, onKeyDown is added to the button
280
+ onKeyDown={(event) => handleTagKeyDown(event, value.id)}
281
+ onPress={() => comboBoxContext.onRemove(new Set([value.id]))}
282
+ />
283
+ </span>
284
+ ))}
285
+
286
+ <div className={cx("relative flex min-w-[20%] flex-1 flex-row items-center", !isSelectionEmpty && "ml-0.5", shortcut && "min-w-[30%]")}>
287
+ <AriaInput
288
+ placeholder={placeholder}
289
+ onKeyDown={handleInputKeyDown}
290
+ onMouseDown={handleInputMouseDown}
291
+ className="w-full flex-[1_0_0] appearance-none bg-transparent text-md text-ellipsis text-primary caret-alpha-black/90 outline-none placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
292
+ />
293
+
294
+ {shortcut && (
295
+ <div
296
+ aria-hidden="true"
297
+ className={cx(
298
+ "absolute inset-y-0.5 right-0.5 z-10 flex items-center rounded-r-[inherit] bg-linear-to-r from-transparent to-bg-primary to-40% pl-8",
299
+ shortcutClassName,
300
+ )}
301
+ >
302
+ <span
303
+ className={cx(
304
+ "pointer-events-none rounded px-1 py-px text-xs font-medium text-quaternary ring-1 ring-secondary select-none ring-inset",
305
+ isDisabled && "bg-transparent text-disabled",
306
+ )}
307
+ >
308
+ ⌘K
309
+ </span>
310
+ </div>
311
+ )}
312
+ </div>
313
+ </div>
314
+ );
315
+ };
316
+
317
+ export const MultiSelectTagsValue = ({
318
+ size,
319
+ shortcut,
320
+ placeholder,
321
+ shortcutClassName,
322
+ placeholderIcon: Icon = SearchLg,
323
+ // Omit this prop to avoid invalid HTML attribute warning
324
+ isDisabled: _isDisabled,
325
+ ...otherProps
326
+ }: ComboBoxValueProps) => {
327
+ return (
328
+ <AriaGroup
329
+ {...otherProps}
330
+ className={({ isFocusWithin, isDisabled }) =>
331
+ cx(
332
+ "relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset",
333
+ isDisabled && "cursor-not-allowed bg-disabled_subtle",
334
+ isFocusWithin && "ring-2 ring-brand",
335
+ sizes[size].root,
336
+ )
337
+ }
338
+ >
339
+ {({ isDisabled }) => (
340
+ <>
341
+ {Icon && <Icon className="pointer-events-none size-5 text-fg-quaternary" />}
342
+ <FocusScope contain={false} autoFocus={false} restoreFocus={false}>
343
+ <InnerMultiSelect
344
+ isDisabled={isDisabled}
345
+ size={size}
346
+ shortcut={shortcut}
347
+ shortcutClassName={shortcutClassName}
348
+ placeholder={placeholder}
349
+ />
350
+ </FocusScope>
351
+ </>
352
+ )}
353
+ </AriaGroup>
354
+ );
355
+ };
356
+
357
+ const MultiSelect = MultiSelectBase as typeof MultiSelectBase & {
358
+ Item: typeof SelectItem;
359
+ };
360
+
361
+ MultiSelect.Item = SelectItem;
362
+
363
+ export { MultiSelect as MultiSelect };
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import type { RefAttributes } from "react";
4
+ import type { PopoverProps as AriaPopoverProps } from "react-aria-components";
5
+ import { Popover as AriaPopover } from "react-aria-components";
6
+ import { cx } from '../../../utils/cx';
7
+
8
+ interface PopoverProps extends AriaPopoverProps, RefAttributes<HTMLElement> {
9
+ size: "sm" | "md";
10
+ }
11
+
12
+ export const Popover = (props: PopoverProps) => {
13
+ return (
14
+ <AriaPopover
15
+ placement="bottom"
16
+ containerPadding={0}
17
+ offset={4}
18
+ {...props}
19
+ className={(state) =>
20
+ cx(
21
+ "max-h-64! w-(--trigger-width) origin-(--trigger-anchor-point) overflow-x-hidden overflow-y-auto rounded-lg bg-primary py-1 shadow-lg ring-1 ring-secondary_alt outline-hidden will-change-transform",
22
+
23
+ state.isEntering &&
24
+ "duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
25
+ state.isExiting &&
26
+ "duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
27
+ props.size === "md" && "max-h-80!",
28
+
29
+ typeof props.className === "function" ? props.className(state) : props.className,
30
+ )
31
+ }
32
+ />
33
+ );
34
+ };
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { isValidElement, useContext } from "react";
4
+ import { Check } from "@untitledui/icons";
5
+ import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components";
6
+ import { ListBoxItem as AriaListBoxItem, Text as AriaText } from "react-aria-components";
7
+ import { Avatar } from '../avatar/avatar';
8
+ import { cx } from '../../../utils/cx';
9
+ import { isReactComponent } from '../../../utils/is-react-component';
10
+ import type { SelectItemType } from "./select";
11
+ import { SelectContext } from "./select";
12
+
13
+ const sizes = {
14
+ sm: "p-2 pr-2.5",
15
+ md: "p-2.5 pl-2",
16
+ };
17
+
18
+ interface SelectItemProps extends Omit<AriaListBoxItemProps<SelectItemType>, "id">, SelectItemType {}
19
+
20
+ export const SelectItem = ({ label, id, value, avatarUrl, supportingText, isDisabled, icon: Icon, className, children, ...props }: SelectItemProps) => {
21
+ const { size } = useContext(SelectContext);
22
+
23
+ const labelOrChildren = label || (typeof children === "string" ? children : "");
24
+ const textValue = supportingText ? labelOrChildren + " " + supportingText : labelOrChildren;
25
+
26
+ return (
27
+ <AriaListBoxItem
28
+ id={id}
29
+ value={
30
+ value ?? {
31
+ id,
32
+ label: labelOrChildren,
33
+ avatarUrl,
34
+ supportingText,
35
+ isDisabled,
36
+ icon: Icon,
37
+ }
38
+ }
39
+ textValue={textValue}
40
+ isDisabled={isDisabled}
41
+ {...props}
42
+ className={(state) => cx("w-full px-1.5 py-px outline-hidden", typeof className === "function" ? className(state) : className)}
43
+ >
44
+ {(state) => (
45
+ <div
46
+ className={cx(
47
+ "flex cursor-pointer items-center gap-2 rounded-md outline-hidden select-none",
48
+ state.isSelected && "bg-active",
49
+ state.isDisabled && "cursor-not-allowed",
50
+ state.isFocused && "bg-primary_hover",
51
+ state.isFocusVisible && "ring-2 ring-focus-ring ring-inset",
52
+
53
+ // Icon styles
54
+ "*:data-icon:size-5 *:data-icon:shrink-0 *:data-icon:text-fg-quaternary",
55
+ state.isDisabled && "*:data-icon:text-fg-disabled",
56
+
57
+ sizes[size],
58
+ )}
59
+ >
60
+ {avatarUrl ? (
61
+ <Avatar aria-hidden="true" size="xs" src={avatarUrl} alt={label} />
62
+ ) : isReactComponent(Icon) ? (
63
+ <Icon data-icon aria-hidden="true" />
64
+ ) : isValidElement(Icon) ? (
65
+ Icon
66
+ ) : null}
67
+
68
+ <div className="flex w-full min-w-0 flex-1 flex-wrap gap-x-2">
69
+ <AriaText
70
+ slot="label"
71
+ className={cx("truncate text-md font-medium whitespace-nowrap text-primary", state.isDisabled && "text-disabled")}
72
+ >
73
+ {label || (typeof children === "function" ? children(state) : children)}
74
+ </AriaText>
75
+
76
+ {supportingText && (
77
+ <AriaText slot="description" className={cx("text-md whitespace-nowrap text-tertiary", state.isDisabled && "text-disabled")}>
78
+ {supportingText}
79
+ </AriaText>
80
+ )}
81
+ </div>
82
+
83
+ {state.isSelected && (
84
+ <Check
85
+ aria-hidden="true"
86
+ className={cx(
87
+ "ml-auto text-fg-brand-primary",
88
+ size === "sm" ? "size-4 stroke-[2.5px]" : "size-5",
89
+ state.isDisabled && "text-fg-disabled",
90
+ )}
91
+ />
92
+ )}
93
+ </div>
94
+ )}
95
+ </AriaListBoxItem>
96
+ );
97
+ };
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { type SelectHTMLAttributes, useId } from "react";
4
+ import { ChevronDown } from "@untitledui/icons";
5
+ import { HintText } from '../input/hint-text';
6
+ import { Label } from '../input/label';
7
+ import { cx } from '../../../utils/cx';
8
+
9
+ interface NativeSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
10
+ label?: string;
11
+ hint?: string;
12
+ selectClassName?: string;
13
+ options: { label: string; value: string; disabled?: boolean }[];
14
+ }
15
+
16
+ export const NativeSelect = ({ label, hint, options, className, selectClassName, ...props }: NativeSelectProps) => {
17
+ const id = useId();
18
+ const selectId = `select-native-${id}`;
19
+ const hintId = `select-native-hint-${id}`;
20
+
21
+ return (
22
+ <div className={cx("w-full in-data-input-wrapper:w-max", className)}>
23
+ {label && (
24
+ <Label htmlFor={selectId} id={selectId} className="mb-1.5">
25
+ {label}
26
+ </Label>
27
+ )}
28
+
29
+ <div className="relative grid w-full items-center">
30
+ <select
31
+ {...props}
32
+ id={selectId}
33
+ aria-describedby={hintId}
34
+ aria-labelledby={selectId}
35
+ className={cx(
36
+ "appearance-none rounded-lg bg-primary px-3.5 py-2.5 text-md font-medium text-primary shadow-xs ring-1 ring-primary outline-hidden transition duration-100 ease-linear ring-inset placeholder:text-fg-quaternary focus-visible:ring-2 focus-visible:ring-brand disabled:cursor-not-allowed disabled:bg-disabled_subtle disabled:text-disabled",
37
+ // Styles when the select is within an `InputGroup`
38
+ "in-data-input-wrapper:flex in-data-input-wrapper:h-full in-data-input-wrapper:gap-1 in-data-input-wrapper:bg-inherit in-data-input-wrapper:px-3 in-data-input-wrapper:py-2 in-data-input-wrapper:font-normal in-data-input-wrapper:text-tertiary in-data-input-wrapper:shadow-none in-data-input-wrapper:ring-transparent",
39
+ // Styles for the select when `TextField` is disabled
40
+ "in-data-input-wrapper:group-disabled:pointer-events-none in-data-input-wrapper:group-disabled:cursor-not-allowed in-data-input-wrapper:group-disabled:bg-transparent in-data-input-wrapper:group-disabled:text-disabled",
41
+ // Common styles for sizes and border radius within `InputGroup`
42
+ "in-data-input-wrapper:in-data-leading:rounded-r-none in-data-input-wrapper:in-data-trailing:rounded-l-none in-data-input-wrapper:in-data-[input-size=md]:py-2.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pl-3.5 in-data-input-wrapper:in-data-[input-size=sm]:py-2 in-data-input-wrapper:in-data-[input-size=sm]:pl-3",
43
+ // For "leading" dropdown within `InputGroup`
44
+ "in-data-input-wrapper:in-data-leading:in-data-[input-size=md]:pr-4.5 in-data-input-wrapper:in-data-leading:in-data-[input-size=sm]:pr-4.5",
45
+ // For "trailing" dropdown within `InputGroup`
46
+ "in-data-input-wrapper:in-data-trailing:in-data-[input-size=md]:pr-8 in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:pr-7.5",
47
+ selectClassName,
48
+ )}
49
+ >
50
+ {options.map((opt) => (
51
+ <option key={opt.value} value={opt.value}>
52
+ {opt.label}
53
+ </option>
54
+ ))}
55
+ </select>
56
+ <ChevronDown
57
+ aria-hidden="true"
58
+ className="pointer-events-none absolute right-3.5 size-5 text-fg-quaternary in-data-input-wrapper:right-0 in-data-input-wrapper:size-4 in-data-input-wrapper:stroke-[2.625px] in-data-input-wrapper:in-data-trailing:in-data-[input-size=sm]:right-3"
59
+ />
60
+ </div>
61
+
62
+ {hint && (
63
+ <HintText className="mt-2" id={hintId}>
64
+ {hint}
65
+ </HintText>
66
+ )}
67
+ </div>
68
+ );
69
+ };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { ChevronDown } from "@untitledui/icons";
5
+ import {
6
+ Button as AriaButton,
7
+ Label as AriaLabel,
8
+ ListBox as AriaListBox,
9
+ ListBoxItem as AriaListBoxItem,
10
+ Popover as AriaPopover,
11
+ Select as AriaSelect,
12
+ SelectValue as AriaSelectValue,
13
+ type SelectProps as AriaSelectProps,
14
+ } from "react-aria-components";
15
+ import { HintText } from '../input/hint-text';
16
+ import { Label } from '../input/label';
17
+ import { cx } from '../../../utils/cx';
18
+
19
+ export interface SelectProps<T extends object> extends Omit<AriaSelectProps<T>, 'children'> {
20
+ label?: string;
21
+ hint?: string;
22
+ placeholder?: string;
23
+ children: React.ReactNode;
24
+ className?: string;
25
+ }
26
+
27
+ export function Select<T extends object>({
28
+ label,
29
+ hint,
30
+ placeholder = "Select an option",
31
+ isInvalid,
32
+ isDisabled,
33
+ isRequired,
34
+ children,
35
+ className,
36
+ ...props
37
+ }: SelectProps<T>) {
38
+ return (
39
+ <AriaSelect
40
+ {...props}
41
+ isInvalid={isInvalid}
42
+ isDisabled={isDisabled}
43
+ isRequired={isRequired}
44
+ className={cx("flex flex-col gap-1.5", className)}
45
+ >
46
+ {label && <Label>{label}</Label>}
47
+ <AriaButton
48
+ className={({ isFocusVisible, isDisabled }) =>
49
+ cx(
50
+ "flex items-center justify-between w-full rounded-sm bg-white px-3 py-2.5 text-base font-body text-fg-primary shadow-sm ring-1 ring-secondary ring-inset transition-shadow duration-100 ease-linear",
51
+ isFocusVisible && !isDisabled && "ring-2 ring-focus-ring",
52
+ isDisabled && "cursor-not-allowed bg-primary text-secondary ring-secondary",
53
+ isInvalid && "ring-error_subtle",
54
+ )
55
+ }
56
+ >
57
+ <AriaSelectValue className="flex-1 text-left placeholder-shown:text-secondary">
58
+ {({ selectedText }) => selectedText || placeholder}
59
+ </AriaSelectValue>
60
+ <ChevronDown className="size-5 text-secondary" />
61
+ </AriaButton>
62
+ {hint && <HintText>{hint}</HintText>}
63
+ <AriaPopover
64
+ className="w-[--trigger-width] rounded-sm bg-white shadow-lg ring-1 ring-secondary overflow-auto max-h-60"
65
+ >
66
+ <AriaListBox className="outline-none p-1">
67
+ {children}
68
+ </AriaListBox>
69
+ </AriaPopover>
70
+ </AriaSelect>
71
+ );
72
+ }
73
+
74
+ import { registerThemeVariant } from '../../../lib/component-registry';
75
+ registerThemeVariant('select', 'aman', Select);