untitledui 0.1.1
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/dist/commands/add.js +339 -0
- package/dist/commands/init.js +436 -0
- package/dist/helper/download-tar-api.js +129 -0
- package/dist/helper/download-tar.js +81 -0
- package/dist/helper/find-css-file.js +19 -0
- package/dist/helper/formatText.js +37 -0
- package/dist/helper/get-components-api.js +47 -0
- package/dist/helper/get-components-list.js +62 -0
- package/dist/helper/get-components.js +19 -0
- package/dist/helper/get-config.js +163 -0
- package/dist/helper/get-package-info.js +99 -0
- package/dist/helper/get-pkg-manager.js +16 -0
- package/dist/helper/get-project.js +176 -0
- package/dist/helper/install-template.js +29 -0
- package/dist/helper/match-color-css.js +82 -0
- package/dist/helper/update-color-css.js +134 -0
- package/dist/index.js +25 -0
- package/dist/package.json +50 -0
- package/dist/res/components.json +520 -0
- package/dist/res/config.json +3 -0
- package/package.json +61 -0
- package/templates/default/.prettierrc +10 -0
- package/templates/default/README.md +36 -0
- package/templates/default/eslint.config.mjs +58 -0
- package/templates/default/next.config.ts +6 -0
- package/templates/default/package.json +57 -0
- package/templates/default/postcss.config.js +5 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/public/marketing/smiling-girl.png +0 -0
- package/templates/default/public/marketing/spirals.webp +0 -0
- package/templates/default/src/app/home-screen.tsx +109 -0
- package/templates/default/src/app/layout.tsx +42 -0
- package/templates/default/src/app/not-found.tsx +40 -0
- package/templates/default/src/app/page.tsx +3 -0
- package/templates/default/src/components/foundations/dot-icon.tsx +27 -0
- package/templates/default/src/components/foundations/featured-icon/featured-icons.tsx +153 -0
- package/templates/default/src/components/foundations/logo/UntitledLogo.tsx +63 -0
- package/templates/default/src/components/foundations/logo/UntitledLogoMinimal.tsx +164 -0
- package/templates/default/src/components/foundations/payment-icons/amex-icon.tsx +19 -0
- package/templates/default/src/components/foundations/payment-icons/apple-pay-icon.tsx +27 -0
- package/templates/default/src/components/foundations/payment-icons/discover-icon.tsx +34 -0
- package/templates/default/src/components/foundations/payment-icons/index.tsx +10 -0
- package/templates/default/src/components/foundations/payment-icons/mastercard-icon.tsx +39 -0
- package/templates/default/src/components/foundations/payment-icons/paypal-icon.tsx +45 -0
- package/templates/default/src/components/foundations/payment-icons/stripe-icon.tsx +27 -0
- package/templates/default/src/components/foundations/payment-icons/union-pay-icon.tsx +37 -0
- package/templates/default/src/components/foundations/payment-icons/visa-icon.tsx +27 -0
- package/templates/default/src/components/marketing/header-navigation/base-components/nav-menu-item.tsx +41 -0
- package/templates/default/src/components/marketing/header-navigation/components/header.tsx +245 -0
- package/templates/default/src/components/marketing/header-navigation/dropdown-header-navigation.tsx +53 -0
- package/templates/default/src/components/shared/avatar/avatar-label-group.tsx +32 -0
- package/templates/default/src/components/shared/avatar/avatar-profile-photo.tsx +84 -0
- package/templates/default/src/components/shared/avatar/avatar.tsx +131 -0
- package/templates/default/src/components/shared/avatar/base-components/avatar-add-button.tsx +33 -0
- package/templates/default/src/components/shared/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/templates/default/src/components/shared/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/templates/default/src/components/shared/avatar/base-components/index.ts +4 -0
- package/templates/default/src/components/shared/avatar/base-components/verified-tick.tsx +34 -0
- package/templates/default/src/components/shared/avatar/utils.ts +12 -0
- package/templates/default/src/components/shared/badges/badge-groups.tsx +176 -0
- package/templates/default/src/components/shared/badges/badge-types.ts +264 -0
- package/templates/default/src/components/shared/badges/badges.tsx +479 -0
- package/templates/default/src/components/shared/button-group/button-group.tsx +97 -0
- package/templates/default/src/components/shared/buttons/app-store-buttons-outline.tsx +454 -0
- package/templates/default/src/components/shared/buttons/app-store-buttons.tsx +806 -0
- package/templates/default/src/components/shared/buttons/button-utility.tsx +87 -0
- package/templates/default/src/components/shared/buttons/button.tsx +284 -0
- package/templates/default/src/components/shared/buttons/close-button.tsx +39 -0
- package/templates/default/src/components/shared/buttons/social-button.tsx +135 -0
- package/templates/default/src/components/shared/buttons/social-logos.tsx +115 -0
- package/templates/default/src/components/shared/checkbox/checkbox.tsx +120 -0
- package/templates/default/src/components/shared/dropdown/dropdown.tsx +138 -0
- package/templates/default/src/components/shared/input-dropdown/combobox.tsx +161 -0
- package/templates/default/src/components/shared/input-dropdown/dropdown-item.tsx +98 -0
- package/templates/default/src/components/shared/input-dropdown/input-dropdown.tsx +172 -0
- package/templates/default/src/components/shared/input-dropdown/multi-select.tsx +373 -0
- package/templates/default/src/components/shared/input-dropdown/popover.tsx +36 -0
- package/templates/default/src/components/shared/input-dropdown/select.tsx +63 -0
- package/templates/default/src/components/shared/inputs/file-upload-trigger.tsx +74 -0
- package/templates/default/src/components/shared/inputs/form/form.tsx +10 -0
- package/templates/default/src/components/shared/inputs/hint-text.tsx +34 -0
- package/templates/default/src/components/shared/inputs/input/index.tsx +189 -0
- package/templates/default/src/components/shared/inputs/input/input-payment.tsx +134 -0
- package/templates/default/src/components/shared/inputs/input/input-with-button.tsx +69 -0
- package/templates/default/src/components/shared/inputs/input/input-with-dropdown.tsx +178 -0
- package/templates/default/src/components/shared/inputs/input/input-with-prefix.tsx +74 -0
- package/templates/default/src/components/shared/inputs/label.tsx +46 -0
- package/templates/default/src/components/shared/inputs/textarea/textarea.tsx +82 -0
- package/templates/default/src/components/shared/progress-indicators/progress-circles.tsx +176 -0
- package/templates/default/src/components/shared/progress-indicators/progress-indicators.tsx +86 -0
- package/templates/default/src/components/shared/progress-indicators/simple-circle.tsx +29 -0
- package/templates/default/src/components/shared/radio-buttons/radio-buttons.tsx +125 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-avatar.tsx +62 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-checkbox.tsx +72 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-icon-card.tsx +95 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-icon-simple.tsx +70 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-payment-icon.tsx +71 -0
- package/templates/default/src/components/shared/radio-groups/radio-group-radio-button.tsx +76 -0
- package/templates/default/src/components/shared/radio-groups/radio-groups.tsx +8 -0
- package/templates/default/src/components/shared/slider/slider.tsx +76 -0
- package/templates/default/src/components/shared/tags/base-components/tag-checkbox.tsx +47 -0
- package/templates/default/src/components/shared/tags/base-components/tag-close-x.tsx +34 -0
- package/templates/default/src/components/shared/tags/tags.tsx +162 -0
- package/templates/default/src/components/shared/toggle/toggle.tsx +140 -0
- package/templates/default/src/components/shared/tooltips/tooltips.tsx +140 -0
- package/templates/default/src/components/utils/index.ts +48 -0
- package/templates/default/src/components/utils/isDeepEqual.ts +31 -0
- package/templates/default/src/components/utils/isReactComponent.ts +22 -0
- package/templates/default/src/components/utils/mergeRefs.ts +19 -0
- package/templates/default/src/components/utils/useBreakpoint.ts +36 -0
- package/templates/default/src/components/utils/uuid.ts +9 -0
- package/templates/default/src/fonts/GeistMonoVF.woff +0 -0
- package/templates/default/src/fonts/GeistVF.woff +0 -0
- package/templates/default/src/hooks/use-resize-observer.tsx +55 -0
- package/templates/default/src/providers/theme.tsx +11 -0
- package/templates/default/src/styles/colors.css +805 -0
- package/templates/default/src/styles/globals.css +86 -0
- package/templates/default/src/styles/text-styles.css +177 -0
- package/templates/default/src/styles/theme.css +1310 -0
- package/templates/default/src/styles/typography.css +428 -0
- package/templates/default/tsconfig.json +27 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode, Ref } from "react";
|
|
4
|
+
import { Checkbox as AriaCheckbox, type CheckboxProps as AriaCheckboxProps } from "react-aria-components";
|
|
5
|
+
import { cx } from "@/components/utils";
|
|
6
|
+
|
|
7
|
+
export interface CheckboxBaseProps {
|
|
8
|
+
size?: "sm" | "md";
|
|
9
|
+
className?: string;
|
|
10
|
+
isFocused?: boolean;
|
|
11
|
+
isSelected?: boolean;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
isIndeterminate?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CheckboxBase = ({ className, isFocused, isSelected, isDisabled, isIndeterminate, size = "sm" }: CheckboxBaseProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cx(
|
|
20
|
+
"flex size-4 shrink-0 cursor-pointer appearance-none items-center justify-center rounded bg-primary ring-1 ring-border-primary ring-inset",
|
|
21
|
+
size === "md" && "size-5 rounded-md",
|
|
22
|
+
(isSelected || isIndeterminate) && "bg-brand-solid ring-bg-brand-solid",
|
|
23
|
+
isDisabled && "cursor-not-allowed bg-disabled_subtle ring-border-disabled",
|
|
24
|
+
isFocused && "outline-2 outline-offset-2 outline-focus-ring",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<svg
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
viewBox="0 0 14 14"
|
|
31
|
+
fill="none"
|
|
32
|
+
className={cx(
|
|
33
|
+
"pointer-events-none absolute h-3 w-2.5 text-fg-white opacity-0 transition-inherit-all",
|
|
34
|
+
size === "md" && "size-3.5",
|
|
35
|
+
isIndeterminate && "opacity-100",
|
|
36
|
+
isDisabled && "text-fg-disabled_subtle",
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<path d="M2.91675 7H11.0834" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
40
|
+
</svg>
|
|
41
|
+
|
|
42
|
+
<svg
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
viewBox="0 0 14 14"
|
|
45
|
+
fill="none"
|
|
46
|
+
className={cx(
|
|
47
|
+
"pointer-events-none absolute size-3 text-fg-white opacity-0 transition-inherit-all",
|
|
48
|
+
size === "md" && "size-3.5",
|
|
49
|
+
isSelected && !isIndeterminate && "opacity-100",
|
|
50
|
+
isDisabled && "text-fg-disabled_subtle",
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<path d="M11.6666 3.5L5.24992 9.91667L2.33325 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
CheckboxBase.displayName = "CheckboxBase";
|
|
59
|
+
|
|
60
|
+
interface CheckboxProps extends AriaCheckboxProps {
|
|
61
|
+
ref?: Ref<HTMLLabelElement>;
|
|
62
|
+
size?: "sm" | "md";
|
|
63
|
+
label?: ReactNode;
|
|
64
|
+
hint?: ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Checkbox = ({ label, hint, size = "sm", className, ...ariaCheckboxProps }: CheckboxProps) => {
|
|
68
|
+
const sizes = {
|
|
69
|
+
sm: {
|
|
70
|
+
root: "gap-2",
|
|
71
|
+
textWrapper: "",
|
|
72
|
+
label: "tt-sm-md",
|
|
73
|
+
hint: "tt-sm",
|
|
74
|
+
},
|
|
75
|
+
md: {
|
|
76
|
+
root: "gap-3",
|
|
77
|
+
textWrapper: "gap-0.5",
|
|
78
|
+
label: "tt-md-md",
|
|
79
|
+
hint: "tt-md",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<AriaCheckbox
|
|
85
|
+
{...ariaCheckboxProps}
|
|
86
|
+
className={(state) =>
|
|
87
|
+
cx(
|
|
88
|
+
"flex items-start",
|
|
89
|
+
state.isDisabled && "cursor-not-allowed",
|
|
90
|
+
sizes[size].root,
|
|
91
|
+
typeof className === "function" ? className(state) : className,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
{({ isSelected, isIndeterminate, isDisabled, isFocused }) => (
|
|
96
|
+
<>
|
|
97
|
+
<CheckboxBase
|
|
98
|
+
size={size}
|
|
99
|
+
isSelected={isSelected}
|
|
100
|
+
isIndeterminate={isIndeterminate}
|
|
101
|
+
isDisabled={isDisabled}
|
|
102
|
+
isFocused={isFocused}
|
|
103
|
+
className={label || hint ? "mt-0.5" : ""}
|
|
104
|
+
/>
|
|
105
|
+
{(label || hint) && (
|
|
106
|
+
<div className={cx("inline-flex flex-col", sizes[size].textWrapper)}>
|
|
107
|
+
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
|
|
108
|
+
{hint && (
|
|
109
|
+
<span className={cx("pointer-events-none text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
|
|
110
|
+
{hint}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</AriaCheckbox>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
Checkbox.displayName = "Checkbox";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FC, ReactNode, RefAttributes } from "react";
|
|
4
|
+
import { DotsVertical } from "@untitledui/icons";
|
|
5
|
+
import type {
|
|
6
|
+
ButtonProps as AriaButtonProps,
|
|
7
|
+
MenuProps as AriaMenuProps,
|
|
8
|
+
PopoverProps as AriaPopoverProps,
|
|
9
|
+
MenuItemProps,
|
|
10
|
+
SeparatorProps,
|
|
11
|
+
} from "react-aria-components";
|
|
12
|
+
import { Button as AriaButton, Header, Menu, MenuItem, MenuSection, MenuTrigger, Popover, Separator } from "react-aria-components";
|
|
13
|
+
import { cx } from "@/components/utils";
|
|
14
|
+
|
|
15
|
+
interface DropdownItemProps extends MenuItemProps {
|
|
16
|
+
label?: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
addon?: string;
|
|
19
|
+
unstyled?: boolean;
|
|
20
|
+
icon?: FC<{ className?: string }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DropdownItem = ({ label, children, addon, icon: Icon, unstyled, ...props }: DropdownItemProps) => {
|
|
24
|
+
if (unstyled) {
|
|
25
|
+
return <MenuItem id={label} textValue={label} {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<MenuItem
|
|
30
|
+
{...props}
|
|
31
|
+
id={label}
|
|
32
|
+
textValue={label}
|
|
33
|
+
className={(values) =>
|
|
34
|
+
cx(
|
|
35
|
+
"group block cursor-pointer px-1.5 py-px outline-hidden",
|
|
36
|
+
values.isDisabled && "cursor-not-allowed",
|
|
37
|
+
typeof props.className === "function" ? props.className(values) : props.className,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
{({ isDisabled, isFocused, isFocusVisible }) => (
|
|
42
|
+
<div
|
|
43
|
+
className={cx(
|
|
44
|
+
"relative flex items-center rounded-md px-2.5 py-2 outline-focus-ring transition duration-100 ease-linear",
|
|
45
|
+
!isDisabled && "group-hover:bg-primary_hover",
|
|
46
|
+
isFocused && "bg-primary_hover",
|
|
47
|
+
isFocusVisible && "outline-2 -outline-offset-2",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{Icon && <Icon className={cx("mr-2 size-4 shrink-0", isDisabled ? "text-fg-disabled" : "text-fg-quaternary")} aria-hidden="true" />}
|
|
51
|
+
|
|
52
|
+
<span className={cx("grow truncate tt-sm-semi", isDisabled ? "text-disabled" : "text-secondary", isFocused && "text-secondary_hover")}>
|
|
53
|
+
{label || children}
|
|
54
|
+
</span>
|
|
55
|
+
|
|
56
|
+
{addon && (
|
|
57
|
+
<span
|
|
58
|
+
className={cx(
|
|
59
|
+
"ml-3 shrink-0 rounded px-1 py-px tt-xs-md ring-1 ring-border-secondary ring-inset",
|
|
60
|
+
isDisabled ? "text-disabled" : "text-quaternary",
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{addon}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</MenuItem>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
interface DropdownMenuProps<T extends object> extends AriaMenuProps<T> {}
|
|
73
|
+
|
|
74
|
+
const DropdownMenu = <T extends object>(props: DropdownMenuProps<T>) => {
|
|
75
|
+
return (
|
|
76
|
+
<Menu
|
|
77
|
+
selectionMode="single"
|
|
78
|
+
disallowEmptySelection
|
|
79
|
+
{...props}
|
|
80
|
+
className={cx("h-min overflow-y-auto py-1 outline-hidden select-none", props.className)}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
interface DropdownPopoverProps extends AriaPopoverProps {}
|
|
86
|
+
|
|
87
|
+
const DropdownPopover = (props: DropdownPopoverProps) => {
|
|
88
|
+
return (
|
|
89
|
+
<Popover
|
|
90
|
+
placement="bottom right"
|
|
91
|
+
{...props}
|
|
92
|
+
className={(state) =>
|
|
93
|
+
cx(
|
|
94
|
+
"w-[248px] rounded-lg bg-primary shadow-lg ring-1 ring-border-secondary_alt will-change-transform",
|
|
95
|
+
state.isEntering &&
|
|
96
|
+
"ease-out animate-in animation-duration-150 fade-in zoom-in-95 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",
|
|
97
|
+
state.isExiting &&
|
|
98
|
+
"animation-duration-100 ease-in animate-out fade-out zoom-out-95 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",
|
|
99
|
+
typeof props.className === "function" ? props.className(state) : props.className,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const DropdownSeparator = (props: SeparatorProps) => {
|
|
107
|
+
return <Separator {...props} className={cx("my-1 h-px w-full bg-border-secondary", props.className)} />;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const DropdownDotsButton = (props: AriaButtonProps & RefAttributes<HTMLButtonElement>) => {
|
|
111
|
+
return (
|
|
112
|
+
<AriaButton
|
|
113
|
+
{...props}
|
|
114
|
+
aria-label="Open menu"
|
|
115
|
+
className={(state) =>
|
|
116
|
+
cx(
|
|
117
|
+
"cursor-pointer rounded-md text-fg-quaternary outline-focus-ring transition duration-100 ease-in-out ease-linear",
|
|
118
|
+
(state.isPressed || state.isHovered) && "text-fg-quaternary_hover",
|
|
119
|
+
(state.isPressed || state.isFocused) && "outline-2 outline-offset-2",
|
|
120
|
+
typeof props.className === "function" ? props.className(state) : props.className,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<DotsVertical className="size-5 transition-inherit-all" />
|
|
125
|
+
</AriaButton>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const Dropdown = {
|
|
130
|
+
Root: MenuTrigger,
|
|
131
|
+
Popover: DropdownPopover,
|
|
132
|
+
Menu: DropdownMenu,
|
|
133
|
+
Section: MenuSection,
|
|
134
|
+
SectionHeader: Header,
|
|
135
|
+
Item: DropdownItem,
|
|
136
|
+
Separator: DropdownSeparator,
|
|
137
|
+
DotsButton: DropdownDotsButton,
|
|
138
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
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";
|
|
17
|
+
import { useResizeObserver } from "@/hooks/use-resize-observer";
|
|
18
|
+
import HintText from "../inputs/hint-text";
|
|
19
|
+
import Label from "../inputs/label";
|
|
20
|
+
import { type CommonProps, SelectContext, type SelectValueType, sizes } from "./input-dropdown";
|
|
21
|
+
import { ComboBoxTagsValue } from "./multi-select";
|
|
22
|
+
import { Popover } from "./popover";
|
|
23
|
+
|
|
24
|
+
type ComboBoxTypes = "search" | "tags";
|
|
25
|
+
|
|
26
|
+
interface ComboBoxProps extends Omit<AriaComboBoxProps<SelectValueType>, "children" | "items">, RefAttributes<HTMLDivElement>, CommonProps {
|
|
27
|
+
shortcut?: boolean;
|
|
28
|
+
type?: ComboBoxTypes;
|
|
29
|
+
items?: SelectValueType[];
|
|
30
|
+
popoverClassName?: string;
|
|
31
|
+
shortcutClassName?: string;
|
|
32
|
+
children: AriaListBoxProps<SelectValueType>["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
|
+
const itemsWithId = items?.map((item) => ({ ...item, id: item.value }));
|
|
113
|
+
|
|
114
|
+
// Resize observer for popover width
|
|
115
|
+
const onResize = useCallback(() => {
|
|
116
|
+
if (!placeholderRef.current) return;
|
|
117
|
+
let divRect = placeholderRef.current?.getBoundingClientRect();
|
|
118
|
+
setPopoverWidth(divRect.width + "px");
|
|
119
|
+
}, [placeholderRef, setPopoverWidth]);
|
|
120
|
+
|
|
121
|
+
useResizeObserver({
|
|
122
|
+
ref: placeholderRef,
|
|
123
|
+
onResize: onResize,
|
|
124
|
+
box: "border-box",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<SelectContext.Provider value={{ type, size }}>
|
|
129
|
+
<AriaComboBox menuTrigger="focus" {...rest}>
|
|
130
|
+
{(state) => (
|
|
131
|
+
<div className="flex flex-col gap-1.5">
|
|
132
|
+
{rest.label && (
|
|
133
|
+
<Label isRequired={state.isRequired} tooltip={rest.tooltip}>
|
|
134
|
+
{rest.label}
|
|
135
|
+
</Label>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
<ComboBoxValue
|
|
139
|
+
ref={placeholderRef}
|
|
140
|
+
placeholder={placeholder}
|
|
141
|
+
shortcut={shortcut}
|
|
142
|
+
size={size}
|
|
143
|
+
// This is a workaround to correctly calculating the trigger width
|
|
144
|
+
// while using ResizeObserver wasn't 100% reliable.
|
|
145
|
+
onFocus={onResize}
|
|
146
|
+
onPointerEnter={onResize}
|
|
147
|
+
{...state}
|
|
148
|
+
{...rest}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<Popover size={size} triggerRef={placeholderRef} style={{ width: popoverWidth }} className={rest.popoverClassName}>
|
|
152
|
+
<AriaListBox {...{ items: itemsWithId, children }} className="size-full outline-hidden" />
|
|
153
|
+
</Popover>
|
|
154
|
+
|
|
155
|
+
{rest.hint && <HintText isInvalid={state.isInvalid}>{rest.hint}</HintText>}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</AriaComboBox>
|
|
159
|
+
</SelectContext.Provider>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { Check, User01 } from "@untitledui/icons";
|
|
5
|
+
import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components";
|
|
6
|
+
import { ListBoxItem as AriaListBoxItem, Text } from "react-aria-components";
|
|
7
|
+
import Dot from "@/components/foundations/dot-icon";
|
|
8
|
+
import { cx } from "@/components/utils";
|
|
9
|
+
import Avatar from "../avatar/avatar";
|
|
10
|
+
import type { IconComponentType } from "../badges/badge-types";
|
|
11
|
+
import type { SelectValueType } from "./input-dropdown";
|
|
12
|
+
import { SelectContext } from "./input-dropdown";
|
|
13
|
+
|
|
14
|
+
export interface ListBoxItemIconProps {
|
|
15
|
+
size?: "sm" | "md";
|
|
16
|
+
isDisabled?: boolean;
|
|
17
|
+
icon?: IconComponentType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const icons = {
|
|
21
|
+
default: () => <></>,
|
|
22
|
+
search: () => <></>,
|
|
23
|
+
tags: ({ isDisabled }: ListBoxItemIconProps) => (
|
|
24
|
+
<User01 aria-hidden="true" className={cx("size-5 shrink-0 text-fg-quaternary", isDisabled && "text-fg-disabled")} />
|
|
25
|
+
),
|
|
26
|
+
avatarLeading: ({ isDisabled }: ListBoxItemIconProps) => (
|
|
27
|
+
<User01 aria-hidden="true" className={cx("size-5 shrink-0 text-fg-quaternary", isDisabled && "text-fg-disabled")} />
|
|
28
|
+
),
|
|
29
|
+
iconLeading: ({ icon: Icon = User01, isDisabled }: ListBoxItemIconProps) => (
|
|
30
|
+
<Icon aria-hidden="true" className={cx("size-5 shrink-0 text-fg-quaternary", isDisabled && "text-fg-disabled")} />
|
|
31
|
+
),
|
|
32
|
+
dotLeading: ({ icon: Icon, isDisabled }: ListBoxItemIconProps) =>
|
|
33
|
+
Icon ? (
|
|
34
|
+
<Icon aria-hidden="true" className={cx("shrink-0 text-fg-success-secondary", isDisabled && "text-fg-disabled_subtle")} />
|
|
35
|
+
) : (
|
|
36
|
+
<Dot size="md" className={cx("shrink-0 text-fg-success-secondary", isDisabled && "text-fg-disabled_subtle")} />
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface DropdownItemProps extends AriaListBoxItemProps<SelectValueType> {
|
|
41
|
+
item: SelectValueType;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const DropdownItem = ({ item, className, ...props }: DropdownItemProps) => {
|
|
45
|
+
const { type, size } = useContext(SelectContext);
|
|
46
|
+
const listItem = { ...item, icon: item?.icon };
|
|
47
|
+
|
|
48
|
+
const sizes = {
|
|
49
|
+
sm: "p-2 pr-2.5",
|
|
50
|
+
md: "p-2.5 pl-2",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const Icon = icons[type];
|
|
54
|
+
|
|
55
|
+
const textValue = listItem?.supportingText ? listItem.label + " " + listItem.supportingText : listItem.label;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<AriaListBoxItem
|
|
59
|
+
value={listItem}
|
|
60
|
+
id={listItem.value}
|
|
61
|
+
key={listItem.value}
|
|
62
|
+
textValue={textValue}
|
|
63
|
+
isDisabled={listItem.isDisabled}
|
|
64
|
+
{...props}
|
|
65
|
+
className={(states) => cx("w-full px-1.5 py-px outline-hidden", typeof className === "function" ? className(states) : className)}
|
|
66
|
+
>
|
|
67
|
+
{({ isSelected, isFocused, isFocusVisible, isHovered, isDisabled }) => (
|
|
68
|
+
<div
|
|
69
|
+
className={cx(
|
|
70
|
+
"flex cursor-pointer items-center gap-2 rounded-md outline-hidden select-none",
|
|
71
|
+
(isSelected || isHovered) && "bg-active",
|
|
72
|
+
isDisabled && "cursor-not-allowed",
|
|
73
|
+
isFocused && "bg-primary_hover",
|
|
74
|
+
isFocusVisible && "ring-2 ring-focus-ring ring-inset",
|
|
75
|
+
sizes[size],
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{"avatarUrl" in listItem ? <Avatar aria-hidden="true" size="xs" src={listItem.avatarUrl} alt={listItem.label} /> : <Icon {...listItem} />}
|
|
79
|
+
|
|
80
|
+
<section className="flex w-full min-w-0 flex-1 flex-wrap gap-x-2">
|
|
81
|
+
<Text slot="label" className={cx("truncate tt-md-md whitespace-nowrap text-primary", isDisabled && "text-disabled")}>
|
|
82
|
+
{listItem.label}
|
|
83
|
+
</Text>
|
|
84
|
+
|
|
85
|
+
{listItem?.supportingText && (
|
|
86
|
+
<Text slot="description" className={cx("tt-md whitespace-nowrap text-tertiary", isDisabled && "text-disabled")}>
|
|
87
|
+
{listItem.supportingText}
|
|
88
|
+
</Text>
|
|
89
|
+
)}
|
|
90
|
+
</section>
|
|
91
|
+
{isSelected && <Check className={cx("ml-auto size-5 text-fg-brand-primary", isDisabled && "text-fg-disabled")} aria-hidden="true" />}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</AriaListBoxItem>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default DropdownItem;
|