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.
- package/README.md +179 -0
- package/package.json +59 -0
- package/src/contexts/ThemeContext.tsx +34 -0
- package/src/contexts/index.ts +1 -0
- package/src/design_system/elements/IconComponent.tsx +98 -0
- package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
- package/src/design_system/elements/avatar/avatar.tsx +131 -0
- package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
- package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
- package/src/design_system/elements/avatar/utils.ts +12 -0
- package/src/design_system/elements/badges/avatar.tsx +132 -0
- package/src/design_system/elements/badges/badge-groups.tsx +176 -0
- package/src/design_system/elements/badges/badge-types.ts +266 -0
- package/src/design_system/elements/badges/badges.tsx +430 -0
- package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
- package/src/design_system/elements/button-group/button-group.tsx +106 -0
- package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
- package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
- package/src/design_system/elements/buttons/button-utility.tsx +116 -0
- package/src/design_system/elements/buttons/button.aman.tsx +174 -0
- package/src/design_system/elements/buttons/button.tsx +271 -0
- package/src/design_system/elements/buttons/close-button.tsx +42 -0
- package/src/design_system/elements/buttons/round-button.tsx +29 -0
- package/src/design_system/elements/buttons/social-button.tsx +148 -0
- package/src/design_system/elements/buttons/social-logos.tsx +115 -0
- package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
- package/src/design_system/elements/carousel/carousel.tsx +308 -0
- package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
- package/src/design_system/elements/date-picker/calendar.tsx +101 -0
- package/src/design_system/elements/date-picker/cell.tsx +106 -0
- package/src/design_system/elements/date-picker/date-input.tsx +32 -0
- package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
- package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
- package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
- package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
- package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
- package/src/design_system/elements/form/form.tsx +10 -0
- package/src/design_system/elements/form/hook-form.tsx +75 -0
- package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
- package/src/design_system/elements/index.tsx +158 -0
- package/src/design_system/elements/input/hint-text.tsx +33 -0
- package/src/design_system/elements/input/input-group.tsx +133 -0
- package/src/design_system/elements/input/input.aman.tsx +172 -0
- package/src/design_system/elements/input/input.tsx +271 -0
- package/src/design_system/elements/input/label.tsx +50 -0
- package/src/design_system/elements/label/label.tsx +50 -0
- package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
- package/src/design_system/elements/map/GoogleMap.tsx +286 -0
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
- package/src/design_system/elements/modals/modal.tsx +41 -0
- package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
- package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
- package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
- package/src/design_system/elements/pagination/pagination.tsx +330 -0
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
- package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
- package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
- package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
- package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
- package/src/design_system/elements/rating/rating-badge.tsx +144 -0
- package/src/design_system/elements/rating/rating-stars.tsx +77 -0
- package/src/design_system/elements/select/combobox.tsx +152 -0
- package/src/design_system/elements/select/multi-select.tsx +363 -0
- package/src/design_system/elements/select/popover.tsx +34 -0
- package/src/design_system/elements/select/select-item.tsx +97 -0
- package/src/design_system/elements/select/select-native.tsx +69 -0
- package/src/design_system/elements/select/select.aman.tsx +75 -0
- package/src/design_system/elements/select/select.tsx +146 -0
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
- package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
- package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
- package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
- package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
- package/src/design_system/elements/tabs/tabs.tsx +225 -0
- package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
- package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
- package/src/design_system/elements/tags/tags.tsx +176 -0
- package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
- package/src/design_system/elements/textarea/textarea.tsx +111 -0
- package/src/design_system/elements/toggle/toggle.tsx +140 -0
- package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
- package/src/design_system/hooks/use-breakpoint.ts +37 -0
- package/src/design_system/hooks/use-resize-observer.ts +68 -0
- package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
- package/src/design_system/logo/keystone-logo.tsx +22 -0
- package/src/design_system/sections/about-home.aman.tsx +85 -0
- package/src/design_system/sections/about-home.tsx +115 -0
- package/src/design_system/sections/blog-cards.tsx +848 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
- package/src/design_system/sections/blog-gallery.tsx +204 -0
- package/src/design_system/sections/blog-home.aman.tsx +84 -0
- package/src/design_system/sections/blog-home.tsx +153 -0
- package/src/design_system/sections/blog-post.aman.tsx +74 -0
- package/src/design_system/sections/blog-post.tsx +301 -0
- package/src/design_system/sections/blog-section.aman.tsx +101 -0
- package/src/design_system/sections/blog-section.tsx +179 -0
- package/src/design_system/sections/contact-home.tsx +25 -0
- package/src/design_system/sections/contact-section.aman.tsx +173 -0
- package/src/design_system/sections/contact-section.tsx +143 -0
- package/src/design_system/sections/faq-grid.aman.tsx +79 -0
- package/src/design_system/sections/faq-grid.tsx +102 -0
- package/src/design_system/sections/faq-home.aman.tsx +92 -0
- package/src/design_system/sections/faq-home.tsx +134 -0
- package/src/design_system/sections/feature-tab.tsx +43 -0
- package/src/design_system/sections/feature-text.tsx +284 -0
- package/src/design_system/sections/footer-home.aman.tsx +62 -0
- package/src/design_system/sections/footer-home.tsx +259 -0
- package/src/design_system/sections/generic-header-component.tsx +103 -0
- package/src/design_system/sections/header-navigation.aman.tsx +360 -0
- package/src/design_system/sections/header-navigation.tsx +334 -0
- package/src/design_system/sections/hero-faq.aman.tsx +38 -0
- package/src/design_system/sections/hero-faq.tsx +55 -0
- package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
- package/src/design_system/sections/hero-generic-text.tsx +51 -0
- package/src/design_system/sections/hero-home.aman.tsx +84 -0
- package/src/design_system/sections/hero-home.tsx +246 -0
- package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
- package/src/design_system/sections/hero-location-detail.tsx +72 -0
- package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
- package/src/design_system/sections/hero-service-detail.tsx +51 -0
- package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
- package/src/design_system/sections/hero-social-media.tsx +35 -0
- package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
- package/src/design_system/sections/hero-testimonials.tsx +55 -0
- package/src/design_system/sections/home-hero-component.tsx +228 -0
- package/src/design_system/sections/index.tsx +131 -0
- package/src/design_system/sections/job-gallery.aman.tsx +91 -0
- package/src/design_system/sections/job-gallery.tsx +183 -0
- package/src/design_system/sections/location-details-section.aman.tsx +179 -0
- package/src/design_system/sections/location-details-section.tsx +196 -0
- package/src/design_system/sections/location-grid.aman.tsx +76 -0
- package/src/design_system/sections/location-grid.tsx +123 -0
- package/src/design_system/sections/services-grid.aman.tsx +85 -0
- package/src/design_system/sections/services-grid.tsx +104 -0
- package/src/design_system/sections/services-home.aman.tsx +78 -0
- package/src/design_system/sections/services-home.tsx +131 -0
- package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
- package/src/design_system/sections/social-media-grid.tsx +189 -0
- package/src/design_system/sections/statistics-section.aman.tsx +79 -0
- package/src/design_system/sections/statistics-section.tsx +97 -0
- package/src/design_system/sections/team-grid.aman.tsx +85 -0
- package/src/design_system/sections/team-grid.tsx +88 -0
- package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
- package/src/design_system/sections/testimonials-home.tsx +90 -0
- package/src/design_system/sections/values-section.aman.tsx +73 -0
- package/src/design_system/sections/values-section.tsx +128 -0
- package/src/design_system/utils/icon-mapping.tsx +28 -0
- package/src/index.ts +7 -0
- package/src/lib/component-registry.ts +53 -0
- package/src/lib/hooks/index.ts +8 -0
- package/src/lib/hooks/use-breakpoint.ts +37 -0
- package/src/lib/hooks/use-clipboard.ts +79 -0
- package/src/lib/hooks/use-resize-observer.ts +68 -0
- package/src/lib/server-api.ts +115 -0
- package/src/styles/style-overrides.aman.css +101 -0
- package/src/styles/theme.css +224 -0
- package/src/styles/typography.css +430 -0
- package/src/themes/index.ts +23 -0
- package/src/types/api/blog-post.ts +53 -0
- package/src/types/api/company-information.ts +44 -0
- package/src/types/api/contact.ts +63 -0
- package/src/types/api/faq.ts +37 -0
- package/src/types/api/job-posting.ts +34 -0
- package/src/types/api/location.ts +36 -0
- package/src/types/api/photos.ts +28 -0
- package/src/types/api/service.ts +37 -0
- package/src/types/api/social-post.ts +28 -0
- package/src/types/api/team-member.ts +29 -0
- package/src/types/api/testimonial.ts +29 -0
- package/src/types/api/website-photos.ts +22 -0
- package/src/types/config.ts +21 -0
- package/src/types/index.ts +21 -0
- package/src/utils/countries.tsx +1351 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/gradient-placeholder.ts +59 -0
- package/src/utils/is-react-component.ts +33 -0
- package/src/utils/markdown-toc.ts +54 -0
- 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);
|