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.
Files changed (121) hide show
  1. package/dist/commands/add.js +339 -0
  2. package/dist/commands/init.js +436 -0
  3. package/dist/helper/download-tar-api.js +129 -0
  4. package/dist/helper/download-tar.js +81 -0
  5. package/dist/helper/find-css-file.js +19 -0
  6. package/dist/helper/formatText.js +37 -0
  7. package/dist/helper/get-components-api.js +47 -0
  8. package/dist/helper/get-components-list.js +62 -0
  9. package/dist/helper/get-components.js +19 -0
  10. package/dist/helper/get-config.js +163 -0
  11. package/dist/helper/get-package-info.js +99 -0
  12. package/dist/helper/get-pkg-manager.js +16 -0
  13. package/dist/helper/get-project.js +176 -0
  14. package/dist/helper/install-template.js +29 -0
  15. package/dist/helper/match-color-css.js +82 -0
  16. package/dist/helper/update-color-css.js +134 -0
  17. package/dist/index.js +25 -0
  18. package/dist/package.json +50 -0
  19. package/dist/res/components.json +520 -0
  20. package/dist/res/config.json +3 -0
  21. package/package.json +61 -0
  22. package/templates/default/.prettierrc +10 -0
  23. package/templates/default/README.md +36 -0
  24. package/templates/default/eslint.config.mjs +58 -0
  25. package/templates/default/next.config.ts +6 -0
  26. package/templates/default/package.json +57 -0
  27. package/templates/default/postcss.config.js +5 -0
  28. package/templates/default/public/favicon.ico +0 -0
  29. package/templates/default/public/marketing/smiling-girl.png +0 -0
  30. package/templates/default/public/marketing/spirals.webp +0 -0
  31. package/templates/default/src/app/home-screen.tsx +109 -0
  32. package/templates/default/src/app/layout.tsx +42 -0
  33. package/templates/default/src/app/not-found.tsx +40 -0
  34. package/templates/default/src/app/page.tsx +3 -0
  35. package/templates/default/src/components/foundations/dot-icon.tsx +27 -0
  36. package/templates/default/src/components/foundations/featured-icon/featured-icons.tsx +153 -0
  37. package/templates/default/src/components/foundations/logo/UntitledLogo.tsx +63 -0
  38. package/templates/default/src/components/foundations/logo/UntitledLogoMinimal.tsx +164 -0
  39. package/templates/default/src/components/foundations/payment-icons/amex-icon.tsx +19 -0
  40. package/templates/default/src/components/foundations/payment-icons/apple-pay-icon.tsx +27 -0
  41. package/templates/default/src/components/foundations/payment-icons/discover-icon.tsx +34 -0
  42. package/templates/default/src/components/foundations/payment-icons/index.tsx +10 -0
  43. package/templates/default/src/components/foundations/payment-icons/mastercard-icon.tsx +39 -0
  44. package/templates/default/src/components/foundations/payment-icons/paypal-icon.tsx +45 -0
  45. package/templates/default/src/components/foundations/payment-icons/stripe-icon.tsx +27 -0
  46. package/templates/default/src/components/foundations/payment-icons/union-pay-icon.tsx +37 -0
  47. package/templates/default/src/components/foundations/payment-icons/visa-icon.tsx +27 -0
  48. package/templates/default/src/components/marketing/header-navigation/base-components/nav-menu-item.tsx +41 -0
  49. package/templates/default/src/components/marketing/header-navigation/components/header.tsx +245 -0
  50. package/templates/default/src/components/marketing/header-navigation/dropdown-header-navigation.tsx +53 -0
  51. package/templates/default/src/components/shared/avatar/avatar-label-group.tsx +32 -0
  52. package/templates/default/src/components/shared/avatar/avatar-profile-photo.tsx +84 -0
  53. package/templates/default/src/components/shared/avatar/avatar.tsx +131 -0
  54. package/templates/default/src/components/shared/avatar/base-components/avatar-add-button.tsx +33 -0
  55. package/templates/default/src/components/shared/avatar/base-components/avatar-company-icon.tsx +26 -0
  56. package/templates/default/src/components/shared/avatar/base-components/avatar-online-indicator.tsx +31 -0
  57. package/templates/default/src/components/shared/avatar/base-components/index.ts +4 -0
  58. package/templates/default/src/components/shared/avatar/base-components/verified-tick.tsx +34 -0
  59. package/templates/default/src/components/shared/avatar/utils.ts +12 -0
  60. package/templates/default/src/components/shared/badges/badge-groups.tsx +176 -0
  61. package/templates/default/src/components/shared/badges/badge-types.ts +264 -0
  62. package/templates/default/src/components/shared/badges/badges.tsx +479 -0
  63. package/templates/default/src/components/shared/button-group/button-group.tsx +97 -0
  64. package/templates/default/src/components/shared/buttons/app-store-buttons-outline.tsx +454 -0
  65. package/templates/default/src/components/shared/buttons/app-store-buttons.tsx +806 -0
  66. package/templates/default/src/components/shared/buttons/button-utility.tsx +87 -0
  67. package/templates/default/src/components/shared/buttons/button.tsx +284 -0
  68. package/templates/default/src/components/shared/buttons/close-button.tsx +39 -0
  69. package/templates/default/src/components/shared/buttons/social-button.tsx +135 -0
  70. package/templates/default/src/components/shared/buttons/social-logos.tsx +115 -0
  71. package/templates/default/src/components/shared/checkbox/checkbox.tsx +120 -0
  72. package/templates/default/src/components/shared/dropdown/dropdown.tsx +138 -0
  73. package/templates/default/src/components/shared/input-dropdown/combobox.tsx +161 -0
  74. package/templates/default/src/components/shared/input-dropdown/dropdown-item.tsx +98 -0
  75. package/templates/default/src/components/shared/input-dropdown/input-dropdown.tsx +172 -0
  76. package/templates/default/src/components/shared/input-dropdown/multi-select.tsx +373 -0
  77. package/templates/default/src/components/shared/input-dropdown/popover.tsx +36 -0
  78. package/templates/default/src/components/shared/input-dropdown/select.tsx +63 -0
  79. package/templates/default/src/components/shared/inputs/file-upload-trigger.tsx +74 -0
  80. package/templates/default/src/components/shared/inputs/form/form.tsx +10 -0
  81. package/templates/default/src/components/shared/inputs/hint-text.tsx +34 -0
  82. package/templates/default/src/components/shared/inputs/input/index.tsx +189 -0
  83. package/templates/default/src/components/shared/inputs/input/input-payment.tsx +134 -0
  84. package/templates/default/src/components/shared/inputs/input/input-with-button.tsx +69 -0
  85. package/templates/default/src/components/shared/inputs/input/input-with-dropdown.tsx +178 -0
  86. package/templates/default/src/components/shared/inputs/input/input-with-prefix.tsx +74 -0
  87. package/templates/default/src/components/shared/inputs/label.tsx +46 -0
  88. package/templates/default/src/components/shared/inputs/textarea/textarea.tsx +82 -0
  89. package/templates/default/src/components/shared/progress-indicators/progress-circles.tsx +176 -0
  90. package/templates/default/src/components/shared/progress-indicators/progress-indicators.tsx +86 -0
  91. package/templates/default/src/components/shared/progress-indicators/simple-circle.tsx +29 -0
  92. package/templates/default/src/components/shared/radio-buttons/radio-buttons.tsx +125 -0
  93. package/templates/default/src/components/shared/radio-groups/radio-group-avatar.tsx +62 -0
  94. package/templates/default/src/components/shared/radio-groups/radio-group-checkbox.tsx +72 -0
  95. package/templates/default/src/components/shared/radio-groups/radio-group-icon-card.tsx +95 -0
  96. package/templates/default/src/components/shared/radio-groups/radio-group-icon-simple.tsx +70 -0
  97. package/templates/default/src/components/shared/radio-groups/radio-group-payment-icon.tsx +71 -0
  98. package/templates/default/src/components/shared/radio-groups/radio-group-radio-button.tsx +76 -0
  99. package/templates/default/src/components/shared/radio-groups/radio-groups.tsx +8 -0
  100. package/templates/default/src/components/shared/slider/slider.tsx +76 -0
  101. package/templates/default/src/components/shared/tags/base-components/tag-checkbox.tsx +47 -0
  102. package/templates/default/src/components/shared/tags/base-components/tag-close-x.tsx +34 -0
  103. package/templates/default/src/components/shared/tags/tags.tsx +162 -0
  104. package/templates/default/src/components/shared/toggle/toggle.tsx +140 -0
  105. package/templates/default/src/components/shared/tooltips/tooltips.tsx +140 -0
  106. package/templates/default/src/components/utils/index.ts +48 -0
  107. package/templates/default/src/components/utils/isDeepEqual.ts +31 -0
  108. package/templates/default/src/components/utils/isReactComponent.ts +22 -0
  109. package/templates/default/src/components/utils/mergeRefs.ts +19 -0
  110. package/templates/default/src/components/utils/useBreakpoint.ts +36 -0
  111. package/templates/default/src/components/utils/uuid.ts +9 -0
  112. package/templates/default/src/fonts/GeistMonoVF.woff +0 -0
  113. package/templates/default/src/fonts/GeistVF.woff +0 -0
  114. package/templates/default/src/hooks/use-resize-observer.tsx +55 -0
  115. package/templates/default/src/providers/theme.tsx +11 -0
  116. package/templates/default/src/styles/colors.css +805 -0
  117. package/templates/default/src/styles/globals.css +86 -0
  118. package/templates/default/src/styles/text-styles.css +177 -0
  119. package/templates/default/src/styles/theme.css +1310 -0
  120. package/templates/default/src/styles/typography.css +428 -0
  121. 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;