untitledui 0.1.5 → 0.1.8

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 (101) hide show
  1. package/config/postcss.config.mjs +6 -0
  2. package/{templates/default/src → config}/styles/globals.css +2 -33
  3. package/{templates/default/src → config}/styles/theme.css +31 -33
  4. package/{templates/default/src → config}/styles/typography.css +24 -24
  5. package/dist/index.mjs +54 -15
  6. package/package.json +4 -3
  7. package/templates/default/.prettierrc +0 -10
  8. package/templates/default/README.md +0 -36
  9. package/templates/default/eslint.config.mjs +0 -58
  10. package/templates/default/next.config.ts +0 -6
  11. package/templates/default/package.json +0 -56
  12. package/templates/default/postcss.config.js +0 -5
  13. package/templates/default/public/favicon.ico +0 -0
  14. package/templates/default/public/marketing/smiling-girl.png +0 -0
  15. package/templates/default/public/marketing/spirals.webp +0 -0
  16. package/templates/default/src/app/home-screen.tsx +0 -108
  17. package/templates/default/src/app/layout.tsx +0 -34
  18. package/templates/default/src/app/not-found.tsx +0 -40
  19. package/templates/default/src/app/page.tsx +0 -3
  20. package/templates/default/src/components/foundations/dot-icon.tsx +0 -27
  21. package/templates/default/src/components/foundations/featured-icon/featured-icons.tsx +0 -153
  22. package/templates/default/src/components/foundations/logo/UntitledLogo.tsx +0 -63
  23. package/templates/default/src/components/foundations/logo/UntitledLogoMinimal.tsx +0 -164
  24. package/templates/default/src/components/foundations/payment-icons/amex-icon.tsx +0 -19
  25. package/templates/default/src/components/foundations/payment-icons/apple-pay-icon.tsx +0 -27
  26. package/templates/default/src/components/foundations/payment-icons/discover-icon.tsx +0 -34
  27. package/templates/default/src/components/foundations/payment-icons/index.tsx +0 -10
  28. package/templates/default/src/components/foundations/payment-icons/mastercard-icon.tsx +0 -39
  29. package/templates/default/src/components/foundations/payment-icons/paypal-icon.tsx +0 -45
  30. package/templates/default/src/components/foundations/payment-icons/stripe-icon.tsx +0 -27
  31. package/templates/default/src/components/foundations/payment-icons/union-pay-icon.tsx +0 -37
  32. package/templates/default/src/components/foundations/payment-icons/visa-icon.tsx +0 -27
  33. package/templates/default/src/components/marketing/header-navigation/base-components/nav-menu-item.tsx +0 -41
  34. package/templates/default/src/components/marketing/header-navigation/components/header.tsx +0 -245
  35. package/templates/default/src/components/marketing/header-navigation/dropdown-header-navigation.tsx +0 -53
  36. package/templates/default/src/components/shared/avatar/avatar-label-group.tsx +0 -32
  37. package/templates/default/src/components/shared/avatar/avatar-profile-photo.tsx +0 -84
  38. package/templates/default/src/components/shared/avatar/avatar.tsx +0 -131
  39. package/templates/default/src/components/shared/avatar/base-components/avatar-add-button.tsx +0 -33
  40. package/templates/default/src/components/shared/avatar/base-components/avatar-company-icon.tsx +0 -26
  41. package/templates/default/src/components/shared/avatar/base-components/avatar-online-indicator.tsx +0 -31
  42. package/templates/default/src/components/shared/avatar/base-components/index.ts +0 -4
  43. package/templates/default/src/components/shared/avatar/base-components/verified-tick.tsx +0 -34
  44. package/templates/default/src/components/shared/avatar/utils.ts +0 -12
  45. package/templates/default/src/components/shared/badges/badge-groups.tsx +0 -176
  46. package/templates/default/src/components/shared/badges/badge-types.ts +0 -264
  47. package/templates/default/src/components/shared/badges/badges.tsx +0 -479
  48. package/templates/default/src/components/shared/button-group/button-group.tsx +0 -97
  49. package/templates/default/src/components/shared/buttons/app-store-buttons-outline.tsx +0 -454
  50. package/templates/default/src/components/shared/buttons/app-store-buttons.tsx +0 -806
  51. package/templates/default/src/components/shared/buttons/button-utility.tsx +0 -87
  52. package/templates/default/src/components/shared/buttons/button.tsx +0 -285
  53. package/templates/default/src/components/shared/buttons/close-button.tsx +0 -39
  54. package/templates/default/src/components/shared/buttons/social-button.tsx +0 -135
  55. package/templates/default/src/components/shared/buttons/social-logos.tsx +0 -115
  56. package/templates/default/src/components/shared/checkbox/checkbox.tsx +0 -120
  57. package/templates/default/src/components/shared/dropdown/dropdown.tsx +0 -147
  58. package/templates/default/src/components/shared/file-upload-trigger/file-upload-trigger.tsx +0 -74
  59. package/templates/default/src/components/shared/form/form.tsx +0 -10
  60. package/templates/default/src/components/shared/form/hook-form.tsx +0 -75
  61. package/templates/default/src/components/shared/input/hint-text.tsx +0 -34
  62. package/templates/default/src/components/shared/input/index.tsx +0 -189
  63. package/templates/default/src/components/shared/input/input-payment.tsx +0 -134
  64. package/templates/default/src/components/shared/input/input-with-button.tsx +0 -69
  65. package/templates/default/src/components/shared/input/input-with-dropdown.tsx +0 -178
  66. package/templates/default/src/components/shared/input/input-with-prefix.tsx +0 -74
  67. package/templates/default/src/components/shared/input/label.tsx +0 -46
  68. package/templates/default/src/components/shared/progress-indicators/progress-circles.tsx +0 -176
  69. package/templates/default/src/components/shared/progress-indicators/progress-indicators.tsx +0 -86
  70. package/templates/default/src/components/shared/progress-indicators/simple-circle.tsx +0 -29
  71. package/templates/default/src/components/shared/radio-buttons/radio-buttons.tsx +0 -125
  72. package/templates/default/src/components/shared/radio-groups/radio-group-avatar.tsx +0 -62
  73. package/templates/default/src/components/shared/radio-groups/radio-group-checkbox.tsx +0 -72
  74. package/templates/default/src/components/shared/radio-groups/radio-group-icon-card.tsx +0 -95
  75. package/templates/default/src/components/shared/radio-groups/radio-group-icon-simple.tsx +0 -70
  76. package/templates/default/src/components/shared/radio-groups/radio-group-payment-icon.tsx +0 -71
  77. package/templates/default/src/components/shared/radio-groups/radio-group-radio-button.tsx +0 -76
  78. package/templates/default/src/components/shared/radio-groups/radio-groups.tsx +0 -8
  79. package/templates/default/src/components/shared/select/combobox.tsx +0 -161
  80. package/templates/default/src/components/shared/select/multi-select.tsx +0 -373
  81. package/templates/default/src/components/shared/select/popover.tsx +0 -36
  82. package/templates/default/src/components/shared/select/select-item.tsx +0 -70
  83. package/templates/default/src/components/shared/select/select-native.tsx +0 -63
  84. package/templates/default/src/components/shared/select/select.tsx +0 -143
  85. package/templates/default/src/components/shared/slider/slider.tsx +0 -76
  86. package/templates/default/src/components/shared/tags/base-components/tag-checkbox.tsx +0 -47
  87. package/templates/default/src/components/shared/tags/base-components/tag-close-x.tsx +0 -34
  88. package/templates/default/src/components/shared/tags/tags.tsx +0 -162
  89. package/templates/default/src/components/shared/textarea/textarea.tsx +0 -82
  90. package/templates/default/src/components/shared/toggle/toggle.tsx +0 -140
  91. package/templates/default/src/components/shared/tooltips/tooltips.tsx +0 -140
  92. package/templates/default/src/components/utils/index.ts +0 -48
  93. package/templates/default/src/components/utils/isDeepEqual.ts +0 -31
  94. package/templates/default/src/components/utils/isReactComponent.ts +0 -22
  95. package/templates/default/src/components/utils/mergeRefs.ts +0 -19
  96. package/templates/default/src/components/utils/useBreakpoint.ts +0 -36
  97. package/templates/default/src/components/utils/uuid.ts +0 -9
  98. package/templates/default/src/hooks/use-resize-observer.tsx +0 -55
  99. package/templates/default/src/providers/theme.tsx +0 -11
  100. package/templates/default/src/styles/text-styles.css +0 -177
  101. package/templates/default/tsconfig.json +0 -27
@@ -1,76 +0,0 @@
1
- "use client";
2
-
3
- import type { FC } from "react";
4
- import type { RadioGroupProps } from "react-aria-components";
5
- import { Label, Radio, RadioGroup, Text } from "react-aria-components";
6
- import { cx } from "@/components/utils/cx";
7
-
8
- type RadioGroupItemType = {
9
- value: string;
10
- title: string;
11
- disabled?: boolean;
12
- description: string;
13
- secondaryTitle: string;
14
- icon: FC<{ className?: string }>;
15
- };
16
-
17
- interface RadioGroupRadioButtonProps extends RadioGroupProps {
18
- size?: "sm" | "md";
19
- items: RadioGroupItemType[];
20
- }
21
-
22
- export const RadioGroupRadioButton = ({ items, size = "sm", className, ...props }: RadioGroupRadioButtonProps) => {
23
- return (
24
- <RadioGroup {...props} className={(states) => cx("flex flex-col gap-3", typeof className === "function" ? className(states) : className)}>
25
- {items.map((plan) => (
26
- <Radio
27
- isDisabled={plan.disabled}
28
- key={plan.value}
29
- value={plan.value}
30
- className={({ isDisabled, isSelected, isFocusVisible }) =>
31
- cx(
32
- "relative flex cursor-pointer rounded-xl bg-primary p-4 transition duration-100 ring-inset",
33
- size === "md" ? "gap-3" : "gap-2",
34
- isSelected ? "ring-2 ring-border-brand" : "ring-1 ring-border-secondary",
35
- isDisabled && "cursor-not-allowed bg-disabled_subtle ring-border-disabled_subtle",
36
- isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
37
- )
38
- }
39
- >
40
- {({ isSelected, isDisabled, isFocusVisible }) => (
41
- <>
42
- <div
43
- className={cx(
44
- "relative mt-0.5 inline-flex shrink-0 items-center justify-center rounded-full transition-inherit-all ring-inset",
45
- size === "md" ? "size-5" : "size-4",
46
- isSelected ? "bg-brand-solid" : "ring-1 ring-border-primary",
47
- isDisabled && "bg-disabled_subtle ring-1 ring-border-disabled",
48
- isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
49
- )}
50
- >
51
- <div
52
- className={cx(
53
- "absolute rounded-full bg-fg-white opacity-0 transition-inherit-all",
54
- size === "md" ? "size-2" : "size-1.5",
55
- isSelected ? "opacity-100" : "opacity-0",
56
- isDisabled && "bg-fg-disabled_subtle",
57
- )}
58
- />
59
- </div>
60
-
61
- <div className={cx("flex flex-col", size === "md" ? "gap-0.5" : "")}>
62
- <Label className={cx("pointer-events-none flex", size === "md" ? "gap-1.5" : "gap-1")}>
63
- <span className={cx("tt-sm-md text-secondary", size === "md" ? "tt-md-md" : "tt-sm-md")}>{plan.title}</span>
64
- <span className={cx("text-tertiary", size === "md" ? "tt-md" : "tt-sm")}>{plan.secondaryTitle}</span>
65
- </Label>
66
- <Text slot="description" className={cx("tt-sm text-tertiary", size === "md" ? "tt-md" : "tt-sm")}>
67
- {plan.description}
68
- </Text>
69
- </div>
70
- </>
71
- )}
72
- </Radio>
73
- ))}
74
- </RadioGroup>
75
- );
76
- };
@@ -1,8 +0,0 @@
1
- "use client";
2
-
3
- export { RadioGroupIconSimple as IconSimple } from "./radio-group-icon-simple";
4
- export { RadioGroupIconCard as IconCard } from "./radio-group-icon-card";
5
- export { RadioGroupAvatar as Avatar } from "./radio-group-avatar";
6
- export { RadioGroupPaymentIcon as PaymentIcon } from "./radio-group-payment-icon";
7
- export { RadioGroupRadioButton as RadioButton } from "./radio-group-radio-button";
8
- export { RadioGroupCheckbox as Checkbox } from "./radio-group-checkbox";
@@ -1,161 +0,0 @@
1
- "use client";
2
-
3
- import type { FocusEventHandler, PointerEventHandler, RefAttributes } from "react";
4
- import { useCallback, useContext, useRef, useState } from "react";
5
- import { SearchLg as SearchIcon } from "@untitledui/icons";
6
- import type { ComboBoxProps as AriaComboBoxProps, ListBoxProps as AriaListBoxProps } from "react-aria-components";
7
- import {
8
- Button as AriaButton,
9
- ComboBox as AriaComboBox,
10
- Group as AriaGroup,
11
- Input as AriaInput,
12
- ListBox as AriaListBox,
13
- ComboBoxStateContext,
14
- } from "react-aria-components";
15
- import { useHotkeys } from "react-hotkeys-hook";
16
- import { cx } from "@/components/utils/cx";
17
- import { useResizeObserver } from "@/hooks/use-resize-observer";
18
- import HintText from "../input/hint-text";
19
- import Label from "../input/label";
20
- import { ComboBoxTagsValue } from "./multi-select";
21
- import { Popover } from "./popover";
22
- import { type CommonProps, SelectContext, type SelectItemType, sizes } from "./select";
23
-
24
- type ComboBoxTypes = "search" | "tags";
25
-
26
- interface ComboBoxProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
27
- shortcut?: boolean;
28
- type?: ComboBoxTypes;
29
- items?: SelectItemType[];
30
- popoverClassName?: string;
31
- shortcutClassName?: string;
32
- children: AriaListBoxProps<SelectItemType>["children"];
33
- }
34
-
35
- interface ComboBoxValueProps extends RefAttributes<HTMLDivElement> {
36
- size: "sm" | "md";
37
- shortcut: boolean;
38
- isDisabled: boolean;
39
- placeholder?: string;
40
- shortcutClassName?: string;
41
- onFocus?: FocusEventHandler;
42
- onPointerEnter?: PointerEventHandler;
43
- }
44
-
45
- const ComboBoxValue = ({ size, isDisabled, shortcut, placeholder, shortcutClassName, ...otherProps }: ComboBoxValueProps) => {
46
- const state = useContext(ComboBoxStateContext);
47
-
48
- const value = state?.selectedItem?.value || null;
49
- const inputValue = state?.inputValue || null;
50
-
51
- const first = inputValue?.split(value?.supportingText)?.[0] || "";
52
- const last = inputValue?.split(first)[1];
53
-
54
- useHotkeys("meta+k", () => state?.setOpen(true), { enabled: !isDisabled && shortcut });
55
-
56
- return (
57
- <AriaGroup
58
- {...otherProps}
59
- className={({ isFocusWithin, isDisabled }) =>
60
- cx(
61
- "relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-border-primary outline-hidden transition-shadow duration-200 ease-in-out ring-inset",
62
- isDisabled && "cursor-not-allowed bg-disabled_subtle",
63
- isFocusWithin && "ring-2 ring-border-brand",
64
- sizes[size].root,
65
- )
66
- }
67
- >
68
- <AriaButton>
69
- <SearchIcon className="size-5 text-fg-quaternary" />
70
- </AriaButton>
71
-
72
- <div className="relative flex w-full items-center gap-2">
73
- {inputValue && (
74
- <span className="absolute top-1/2 z-0 inline-flex w-full -translate-y-1/2 gap-2 truncate" aria-hidden="true">
75
- <p className={cx("tt-md-md text-primary", isDisabled && "text-disabled")}>{first}</p>
76
- {last && <p className={cx("-ml-[3px] tt-md text-tertiary", isDisabled && "text-disabled")}>{last}</p>}
77
- </span>
78
- )}
79
- <AriaInput
80
- placeholder={placeholder}
81
- className="z-10 w-full appearance-none bg-transparent tt-md text-transparent caret-alpha-black/90 placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
82
- />
83
- </div>
84
-
85
- {shortcut && (
86
- <div
87
- className={cx(
88
- "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",
89
- sizes[size].shortcut,
90
- shortcutClassName,
91
- )}
92
- >
93
- <span
94
- className={cx(
95
- "pointer-events-none rounded px-1 py-px tt-xs-md text-quaternary ring-1 ring-border-secondary select-none ring-inset",
96
- isDisabled && "bg-transparent text-disabled",
97
- )}
98
- aria-hidden="true"
99
- >
100
- ⌘K
101
- </span>
102
- </div>
103
- )}
104
- </AriaGroup>
105
- );
106
- };
107
-
108
- export const ComboBox = ({ type = "search", placeholder = "Select", shortcut = true, size = "sm", children, items, ...rest }: ComboBoxProps) => {
109
- const placeholderRef = useRef<HTMLDivElement>(null);
110
- const [popoverWidth, setPopoverWidth] = useState("");
111
-
112
- // Resize observer for popover width
113
- const onResize = useCallback(() => {
114
- if (!placeholderRef.current) return;
115
- let divRect = placeholderRef.current?.getBoundingClientRect();
116
- setPopoverWidth(divRect.width + "px");
117
- }, [placeholderRef, setPopoverWidth]);
118
-
119
- useResizeObserver({
120
- ref: placeholderRef,
121
- onResize: onResize,
122
- box: "border-box",
123
- });
124
-
125
- return (
126
- <SelectContext.Provider value={{ type, size }}>
127
- <AriaComboBox menuTrigger="focus" {...rest}>
128
- {(state) => (
129
- <div className="flex flex-col gap-1.5">
130
- {rest.label && (
131
- <Label isRequired={state.isRequired} tooltip={rest.tooltip}>
132
- {rest.label}
133
- </Label>
134
- )}
135
-
136
- <ComboBoxValue
137
- ref={placeholderRef}
138
- placeholder={placeholder}
139
- shortcut={shortcut}
140
- size={size}
141
- // This is a workaround to correctly calculating the trigger width
142
- // while using ResizeObserver wasn't 100% reliable.
143
- onFocus={onResize}
144
- onPointerEnter={onResize}
145
- {...state}
146
- {...rest}
147
- />
148
-
149
- <Popover size={size} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={rest.popoverClassName}>
150
- <AriaListBox items={items} className="size-full outline-hidden">
151
- {children}
152
- </AriaListBox>
153
- </Popover>
154
-
155
- {rest.hint && <HintText isInvalid={state.isInvalid}>{rest.hint}</HintText>}
156
- </div>
157
- )}
158
- </AriaComboBox>
159
- </SelectContext.Provider>
160
- );
161
- };
@@ -1,373 +0,0 @@
1
- "use client";
2
-
3
- import type { FocusEventHandler, KeyboardEvent, PointerEventHandler, RefAttributes } 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, ListBoxProps as AriaListBoxProps, Key } from "react-aria-components";
8
- import {
9
- Button as AriaButton,
10
- ComboBox as AriaComboBox,
11
- Group as AriaGroup,
12
- Input as AriaInput,
13
- ListBox as AriaListBox,
14
- ComboBoxStateContext,
15
- } from "react-aria-components";
16
- import { useHotkeys } from "react-hotkeys-hook";
17
- import type { ListData } from "react-stately";
18
- import { useListData } from "react-stately";
19
- import { cx } from "@/components/utils/cx";
20
- import { useResizeObserver } from "@/hooks/use-resize-observer";
21
- import Avatar from "../avatar/avatar";
22
- import type { IconComponentType } from "../badges/badge-types";
23
- import HintText from "../input/hint-text";
24
- import Label from "../input/label";
25
- import { TagCloseX } from "../tags/base-components/tag-close-x";
26
- import { Popover } from "./popover";
27
- import { type SelectItemType, sizes } from "./select";
28
- import SelectItem from "./select-item";
29
-
30
- interface ComboBoxValueProps extends RefAttributes<HTMLDivElement> {
31
- size: "sm" | "md";
32
- shortcut?: boolean;
33
- isDisabled?: boolean;
34
- placeholder?: string;
35
- shortcutClassName?: string;
36
- placeholderIcon?: IconComponentType | null;
37
- onFocus?: FocusEventHandler;
38
- onPointerEnter?: PointerEventHandler;
39
- }
40
-
41
- const ComboboxContext = createContext<{
42
- size: "sm" | "md";
43
- selectedKeys: Key[];
44
- selectedItems: ListData<SelectItemType>;
45
- onRemove: (keys: Set<Key>) => void;
46
- onInputChange: (value: string) => void;
47
- }>({
48
- size: "sm",
49
- selectedKeys: [],
50
- selectedItems: {} as ListData<SelectItemType>,
51
- onRemove: () => {},
52
- onInputChange: () => {},
53
- });
54
-
55
- interface CommonProps {
56
- hint?: string;
57
- label?: string;
58
- tooltip?: string;
59
- size?: "sm" | "md";
60
- placeholder?: string;
61
- }
62
-
63
- interface ComboBoxProps extends Omit<AriaComboBoxProps<SelectItemType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
64
- shortcut?: boolean;
65
- items?: SelectItemType[];
66
- popoverClassName?: string;
67
- shortcutClassName?: string;
68
- selectedItems: ListData<SelectItemType>;
69
- placeholderIcon?: IconComponentType | null;
70
- children: AriaListBoxProps<SelectItemType>["children"];
71
- onItemCleared?: (key: Key) => void;
72
- onItemInserted?: (key: Key) => void;
73
- }
74
-
75
- export const ComboBox = ({
76
- name,
77
- items,
78
- children,
79
- className,
80
- size = "sm",
81
- selectedItems,
82
- onItemCleared,
83
- onItemInserted,
84
- shortcut,
85
- placeholder = "Search",
86
- ...props
87
- }: ComboBoxProps) => {
88
- const { contains } = useFilter({ sensitivity: "base" });
89
- const selectedKeys = selectedItems.items.map((i) => i?.id);
90
-
91
- const filter = useCallback(
92
- (item: SelectItemType, filterText: string) => {
93
- return !selectedKeys.includes(item.id) && contains(item.label, filterText);
94
- },
95
- [contains, selectedKeys],
96
- );
97
-
98
- const accessibleList = useListData({
99
- initialItems: items,
100
- filter,
101
- });
102
-
103
- const [fieldState, setFieldState] = useState<{
104
- selectedKey: Key | null;
105
- inputValue: string;
106
- }>({
107
- selectedKey: null,
108
- inputValue: "",
109
- });
110
-
111
- const onRemove = useCallback(
112
- (keys: Set<Key>) => {
113
- const key = keys.values().next().value;
114
-
115
- if (!key) return;
116
-
117
- selectedItems.remove(key);
118
- onItemCleared?.(key);
119
- },
120
- [selectedItems, onItemCleared],
121
- );
122
-
123
- // TODO: `onSelectionChange` is being called on blur thus causing the already deleted item to reappear
124
- const onSelectionChange = (id: Key | null) => {
125
- if (!id) {
126
- return;
127
- }
128
-
129
- const item = accessibleList.getItem(id);
130
-
131
- if (!item) {
132
- return;
133
- }
134
-
135
- if (!selectedKeys.includes(id as string)) {
136
- selectedItems.append(item);
137
- setFieldState({
138
- inputValue: "",
139
- selectedKey: id,
140
- });
141
- onItemInserted?.(id);
142
- }
143
-
144
- accessibleList.setFilterText("");
145
- };
146
-
147
- const onInputChange = (value: string) => {
148
- setFieldState((prev) => ({
149
- inputValue: value,
150
- selectedKey: value === "" ? null : prev.selectedKey,
151
- }));
152
-
153
- accessibleList.setFilterText(value);
154
- };
155
-
156
- const placeholderRef = useRef<HTMLDivElement>(null);
157
- const [popoverWidth, setPopoverWidth] = useState("");
158
-
159
- // Resize observer for popover width
160
- const onResize = useCallback(() => {
161
- if (!placeholderRef.current) return;
162
- let divRect = placeholderRef.current?.getBoundingClientRect();
163
- setPopoverWidth(divRect.width + "px");
164
- }, [placeholderRef, setPopoverWidth]);
165
-
166
- useResizeObserver({
167
- ref: placeholderRef,
168
- onResize: onResize,
169
- box: "border-box",
170
- });
171
-
172
- return (
173
- <ComboboxContext.Provider
174
- value={{
175
- size,
176
- selectedKeys,
177
- selectedItems,
178
- onInputChange,
179
- onRemove,
180
- }}
181
- >
182
- <AriaComboBox
183
- allowsEmptyCollection
184
- menuTrigger="focus"
185
- items={accessibleList.items}
186
- onInputChange={onInputChange}
187
- inputValue={fieldState.inputValue}
188
- selectedKey={fieldState.selectedKey}
189
- onSelectionChange={onSelectionChange}
190
- {...props}
191
- >
192
- {(state) => (
193
- <div className="flex flex-col gap-1.5">
194
- {props.label && (
195
- <Label isRequired={state.isRequired} tooltip={props.tooltip}>
196
- {props.label}
197
- </Label>
198
- )}
199
-
200
- <ComboBoxTagsValue
201
- size={size}
202
- shortcut={shortcut}
203
- ref={placeholderRef}
204
- placeholder={placeholder}
205
- // This is a workaround to correctly calculating the trigger width
206
- // while using ResizeObserver wasn't 100% reliable.
207
- onFocus={onResize}
208
- onPointerEnter={onResize}
209
- {...state}
210
- {...props}
211
- />
212
-
213
- <Popover size={"md"} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={props?.popoverClassName}>
214
- <AriaListBox selectionMode="multiple" className="size-full outline-hidden">
215
- {children}
216
- </AriaListBox>
217
- </Popover>
218
-
219
- {props.hint && <HintText isInvalid={state.isInvalid}>{props.hint}</HintText>}
220
- </div>
221
- )}
222
- </AriaComboBox>
223
- </ComboboxContext.Provider>
224
- );
225
- };
226
-
227
- const InnerComboBox = ({ isDisabled, shortcut, shortcutClassName, placeholder }: ComboBoxValueProps) => {
228
- const focusManager = useFocusManager();
229
- const selectContext = useContext(ComboboxContext);
230
-
231
- const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
232
- const isCaretAtStart = event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;
233
-
234
- if (!isCaretAtStart && event.currentTarget.value !== "") {
235
- return;
236
- }
237
-
238
- switch (event.key) {
239
- case "Backspace":
240
- case "ArrowLeft":
241
- focusManager?.focusPrevious({ wrap: false, tabbable: false });
242
- break;
243
- case "ArrowRight":
244
- focusManager?.focusNext({ wrap: false, tabbable: false });
245
- break;
246
- }
247
- };
248
-
249
- const handleTagKeyDown = (event: KeyboardEvent<HTMLButtonElement>, value: Key) => {
250
- event.preventDefault();
251
-
252
- const isFirstTag = selectContext?.selectedItems?.items?.[0]?.id === value;
253
-
254
- switch (event.key) {
255
- case " ":
256
- case "Enter":
257
- case "Backspace":
258
- if (isFirstTag) {
259
- focusManager?.focusNext({ wrap: false, tabbable: false });
260
- } else {
261
- focusManager?.focusPrevious({ wrap: false, tabbable: false });
262
- }
263
-
264
- selectContext.onRemove(new Set([value]));
265
- break;
266
-
267
- case "ArrowLeft":
268
- focusManager?.focusPrevious({ wrap: false, tabbable: false });
269
- break;
270
- case "ArrowRight":
271
- focusManager?.focusNext({ wrap: false, tabbable: false });
272
- break;
273
- }
274
- };
275
-
276
- const isSelectionEmpty = selectContext?.selectedItems?.items?.length === 0;
277
-
278
- return (
279
- <div className="relative flex w-full flex-1 flex-row flex-wrap items-center justify-start gap-1.5">
280
- {!isSelectionEmpty &&
281
- // TODO: Use <TagList /> here
282
- selectContext?.selectedItems?.items?.map((value) => (
283
- <span key={value.id} className="flex items-center rounded-md bg-primary py-0.5 pr-1 pl-[5px] ring-1 ring-border-primary ring-inset">
284
- <Avatar size="xxs" alt={value?.label} src={value?.avatarUrl} />
285
-
286
- <p className="ml-[5px] truncate tt-sm-md whitespace-nowrap text-secondary select-none">{value?.label}</p>
287
-
288
- <TagCloseX
289
- size="md"
290
- isDisabled={isDisabled}
291
- className="ml-[3px]"
292
- // For workaround, onKeyDown is added to the button
293
- onKeyDown={(event) => handleTagKeyDown(event, value.id)}
294
- onPress={() => selectContext.onRemove(new Set([value.id]))}
295
- />
296
- </span>
297
- ))}
298
-
299
- <div className={cx("relative flex min-w-[20%] flex-1 flex-row items-center", !isSelectionEmpty && "ml-0.5", shortcut && "min-w-[30%]")}>
300
- <AriaInput
301
- placeholder={placeholder}
302
- onKeyDown={handleInputKeyDown}
303
- className="w-full flex-[1_0_0] appearance-none bg-transparent tt-md text-ellipsis text-primary caret-alpha-black/90 placeholder:text-placeholder focus:outline-hidden disabled:cursor-not-allowed disabled:text-disabled disabled:placeholder:text-disabled"
304
- />
305
-
306
- {shortcut && (
307
- <div
308
- aria-hidden="true"
309
- className={cx(
310
- "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",
311
- shortcutClassName,
312
- )}
313
- >
314
- <span
315
- className={cx(
316
- "pointer-events-none rounded px-1 py-px tt-xs-md text-quaternary ring-1 ring-border-secondary select-none ring-inset",
317
- isDisabled && "bg-transparent text-disabled",
318
- )}
319
- >
320
- ⌘K
321
- </span>
322
- </div>
323
- )}
324
- </div>
325
- </div>
326
- );
327
- };
328
-
329
- export const ComboBoxTagsValue = ({
330
- size,
331
- isDisabled,
332
- shortcut,
333
- placeholder,
334
- shortcutClassName,
335
- placeholderIcon: Icon = SearchLg,
336
- ...otherProps
337
- }: ComboBoxValueProps) => {
338
- const state = useContext(ComboBoxStateContext);
339
-
340
- useHotkeys("meta+k", () => state?.setOpen(true), { enabled: !isDisabled && shortcut });
341
-
342
- return (
343
- <AriaGroup
344
- {...otherProps}
345
- className={({ isFocusWithin, isDisabled }) =>
346
- cx(
347
- "relative flex w-full items-center gap-2 rounded-lg bg-primary shadow-xs ring-1 ring-border-primary outline-hidden transition duration-200 ease-in-out ring-inset",
348
- isDisabled && "cursor-not-allowed bg-disabled_subtle",
349
- isFocusWithin && "ring-2 ring-border-brand",
350
- sizes[size].root,
351
- )
352
- }
353
- >
354
- {Icon && (
355
- <AriaButton>
356
- <Icon className="size-5 text-fg-quaternary" />
357
- </AriaButton>
358
- )}
359
-
360
- <FocusScope contain={false} autoFocus={false} restoreFocus={false}>
361
- <InnerComboBox size={size} isDisabled={isDisabled} shortcut={shortcut} shortcutClassName={shortcutClassName} placeholder={placeholder} />
362
- </FocusScope>
363
- </AriaGroup>
364
- );
365
- };
366
-
367
- const MultiSelect = ComboBox as typeof ComboBox & {
368
- Item: typeof SelectItem;
369
- };
370
-
371
- MultiSelect.Item = SelectItem;
372
-
373
- export { MultiSelect as MultiSelect };
@@ -1,36 +0,0 @@
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 "@/components/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) overflow-x-hidden overflow-y-auto rounded-lg bg-primary py-1 shadow-lg ring-1 ring-border-secondary_alt outline-hidden will-change-transform",
22
-
23
- // scrollbar styles
24
- // "[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-alpha-black/15 [&::-webkit-scrollbar-track]:rounded-full [&::-webkit-scrollbar-track]:bg-primary [&::-webkit-scrollbar]:w-2",
25
- state.isEntering &&
26
- "duration-150 ease-out animate-in fade-in placement-right:origin-left placement-right:slide-in-from-left-0.5 placement-top:origin-bottom placement-top:slide-in-from-bottom-0.5 placement-bottom:origin-top placement-bottom:slide-in-from-top-0.5",
27
- state.isExiting &&
28
- "duration-100 ease-in animate-out fade-out placement-right:origin-left placement-right:slide-out-to-left-0.5 placement-top:origin-bottom placement-top:slide-out-to-bottom-0.5 placement-bottom:origin-top placement-bottom:slide-out-to-top-0.5",
29
- props.size === "md" && "max-h-80!",
30
-
31
- typeof props.className === "function" ? props.className(state) : props.className,
32
- )
33
- }
34
- />
35
- );
36
- };