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,245 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useRef, useState } from "react";
5
+ import { ChevronDown } from "@untitledui/icons";
6
+ import { Button as AriaButton, Dialog, DialogTrigger, Popover } from "react-aria-components";
7
+ import UntitledLogo from "@/components/foundations/logo/UntitledLogo";
8
+ import UntitledLogoMinimal from "@/components/foundations/logo/UntitledLogoMinimal";
9
+ import Button from "@/components/shared/buttons/button";
10
+ import { cx } from "@/components/utils";
11
+ import { DropdownMenuSimple } from "../dropdown-header-navigation";
12
+
13
+ type HeaderNavItem = {
14
+ label: string;
15
+ href?: string;
16
+ menu?: ReactNode;
17
+ };
18
+
19
+ const headerNavItems: HeaderNavItem[] = [
20
+ { label: "Products", href: "/products", menu: <DropdownMenuSimple /> },
21
+ { label: "Services", href: "/Services", menu: <DropdownMenuSimple /> },
22
+ { label: "Pricing", href: "/pricing" },
23
+ { label: "Resources", href: "/resources", menu: <DropdownMenuSimple /> },
24
+ { label: "About", href: "/about" },
25
+ ];
26
+
27
+ const footerNavItems = [
28
+ { label: "About us", href: "/" },
29
+ { label: "Press", href: "/products" },
30
+ { label: "Careers", href: "/resources" },
31
+ { label: "Legal", href: "/pricing" },
32
+ { label: "Support", href: "/pricing" },
33
+ { label: "Contact", href: "/pricing" },
34
+ { label: "Sitemap", href: "/pricing" },
35
+ { label: "Cookie settings", href: "/pricing" },
36
+ ];
37
+
38
+ const MobileNavItem = (props: { className?: string; label: string; href?: string; children?: ReactNode }) => {
39
+ const [isOpen, setIsOpen] = useState(false);
40
+
41
+ if (props.href) {
42
+ return (
43
+ <li>
44
+ <a href={props.href} className="flex items-center justify-between px-4 py-3 tt-md-semi text-primary hover:bg-primary_hover">
45
+ {props.label}
46
+ </a>
47
+ </li>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <li className="flex flex-col gap-0.5">
53
+ <button
54
+ aria-expanded={isOpen}
55
+ onClick={() => setIsOpen(!isOpen)}
56
+ className="flex w-full items-center justify-between px-4 py-3 tt-md-semi text-primary hover:bg-primary_hover"
57
+ >
58
+ {props.label} <ChevronDown className={cx(isOpen ? "-rotate-180" : "rotate-0", "size-5 text-quaternary transition duration-200 ease-out")} />
59
+ </button>
60
+ {isOpen && <div>{props.children}</div>}
61
+ </li>
62
+ );
63
+ };
64
+
65
+ const MobileFooter = () => {
66
+ return (
67
+ <div className="flex flex-col gap-8 border-t border-secondary px-4 py-6">
68
+ <div>
69
+ <ul className="grid grid-flow-col grid-cols-2 grid-rows-4 gap-x-6 gap-y-3">
70
+ {footerNavItems.map((navItem) => (
71
+ <li key={navItem.label}>
72
+ <Button color="link-gray" size="lg" href={navItem.href}>
73
+ {navItem.label}
74
+ </Button>
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ </div>
79
+ <div className="flex flex-col gap-3">
80
+ <Button href="/sign-up" size="lg">
81
+ Sign up
82
+ </Button>
83
+ <Button href="/login" color="secondary" size="lg">
84
+ Log in
85
+ </Button>
86
+ </div>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ interface HeaderProps {
92
+ items?: HeaderNavItem[];
93
+ isFullWidth?: boolean;
94
+ isFloating?: boolean;
95
+ className?: string;
96
+ // Used only for easily testing menus in Storybook.
97
+ autoOpenMenu?: boolean;
98
+ }
99
+
100
+ export const Header = ({ items = headerNavItems, isFullWidth, isFloating, className, autoOpenMenu }: HeaderProps) => {
101
+ const headerRef = useRef<HTMLElement>(null);
102
+
103
+ return (
104
+ <header
105
+ ref={headerRef}
106
+ className={cx(
107
+ "relative flex h-[72px] w-full items-center justify-center md:h-20",
108
+ isFloating && "h-16 md:h-[76px] md:pt-3",
109
+ isFullWidth && !isFloating ? "has-aria-expanded:bg-primary" : "max-md:has-aria-expanded:bg-primary",
110
+ className,
111
+ )}
112
+ >
113
+ <div className="flex size-full max-w-container flex-1 items-center pr-3 pl-4 md:px-8">
114
+ <div
115
+ className={cx(
116
+ "flex w-full justify-between",
117
+ isFloating && "ring-border-secondary ring-inset md:rounded-2xl md:bg-primary md:py-3 md:pr-3 md:pl-4 md:shadow-xs md:ring-1",
118
+ )}
119
+ >
120
+ <div className="flex flex-1 items-center gap-8">
121
+ <UntitledLogo className="md:hidden lg:inline-block" />
122
+ <UntitledLogoMinimal className="hidden md:inline-block lg:hidden" />
123
+
124
+ {/* Desktop navigation */}
125
+ <nav className="hidden md:block">
126
+ <ul className="flex items-center gap-5">
127
+ {items.map((navItem) => (
128
+ <li key={navItem.label}>
129
+ {navItem.menu ? (
130
+ <DialogTrigger defaultOpen={autoOpenMenu && navItem.label === "Resources"}>
131
+ <Button
132
+ size="lg"
133
+ color="link-gray"
134
+ className="group gap-1"
135
+ iconTrailing={
136
+ <ChevronDown className="size-5 rotate-0 transition duration-200 ease-out group-aria-expanded:-rotate-180" />
137
+ }
138
+ >
139
+ {navItem.label}
140
+ </Button>
141
+ <Popover
142
+ className={({ isEntering, isExiting }) =>
143
+ cx(
144
+ "hidden will-change-transform md:block",
145
+ isFullWidth && "w-full",
146
+ isEntering && "duration-300 ease-out animate-in fade-in slide-in-from-top-2",
147
+ isExiting && "duration-200 ease-in animate-out fade-out slide-out-to-top-2",
148
+ )
149
+ }
150
+ offset={isFloating || isFullWidth ? 0 : 12}
151
+ containerPadding={0}
152
+ triggerRef={(isFloating && isFullWidth) || isFullWidth ? headerRef : undefined}
153
+ >
154
+ <Dialog className={cx("mx-auto outline-hidden", isFloating && "max-w-7xl px-8 pt-3")}>
155
+ {navItem.menu}
156
+ </Dialog>
157
+ </Popover>
158
+ </DialogTrigger>
159
+ ) : (
160
+ <Button
161
+ href={navItem.href}
162
+ size="lg"
163
+ color="link-gray"
164
+ className="rounded-md outline-focus-ring focus:outline-2 focus:outline-offset-2"
165
+ >
166
+ {navItem.label}
167
+ </Button>
168
+ )}
169
+ </li>
170
+ ))}
171
+ </ul>
172
+ </nav>
173
+ </div>
174
+
175
+ <div className="hidden items-center gap-3 md:flex">
176
+ <Button href="/login" color="secondary" size={isFloating ? "md" : "lg"}>
177
+ Log in
178
+ </Button>
179
+ <Button href="/sign-up" color="primary" size={isFloating ? "md" : "lg"}>
180
+ Sign up
181
+ </Button>
182
+ </div>
183
+
184
+ {/* Mobile Menu and Menu Trigger */}
185
+ <DialogTrigger>
186
+ <AriaButton
187
+ className={({ isFocused, isHovered }) =>
188
+ cx(
189
+ "group ml-auto rounded-lg p-2 md:hidden",
190
+ isHovered && "bg-primary_hover",
191
+ isFocused && "outline-2 outline-offset-2 outline-focus-ring",
192
+ )
193
+ }
194
+ >
195
+ <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none">
196
+ <path
197
+ className="hidden text-secondary group-aria-expanded:block"
198
+ d="M18 6L6 18M6 6L18 18"
199
+ stroke="currentColor"
200
+ strokeWidth="2"
201
+ strokeLinecap="round"
202
+ strokeLinejoin="round"
203
+ />
204
+ <path
205
+ className="text-secondary group-aria-expanded:hidden"
206
+ d="M3 12H21M3 6H21M3 18H21"
207
+ stroke="currentColor"
208
+ strokeWidth="2"
209
+ strokeLinecap="round"
210
+ strokeLinejoin="round"
211
+ />
212
+ </svg>
213
+ </AriaButton>
214
+ <Popover
215
+ triggerRef={headerRef}
216
+ className="h-calc(100%-72px) scrollbar-hide w-full overflow-y-auto shadow-lg md:hidden"
217
+ offset={0}
218
+ crossOffset={20}
219
+ containerPadding={0}
220
+ placement="bottom left"
221
+ >
222
+ <Dialog className="outline-hidden">
223
+ <nav className="w-full bg-primary shadow-lg">
224
+ <ul className="flex flex-col gap-0.5 py-5">
225
+ {items.map((navItem) =>
226
+ navItem.menu ? (
227
+ <MobileNavItem key={navItem.label} label={navItem.label}>
228
+ {navItem.menu}
229
+ </MobileNavItem>
230
+ ) : (
231
+ <MobileNavItem key={navItem.label} label={navItem.label} href={navItem.href} />
232
+ ),
233
+ )}
234
+ </ul>
235
+
236
+ <MobileFooter />
237
+ </nav>
238
+ </Dialog>
239
+ </Popover>
240
+ </DialogTrigger>
241
+ </div>
242
+ </div>
243
+ </header>
244
+ );
245
+ };
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { BookClosed, FileCode01, LifeBuoy01, PlayCircle, Stars02 } from "@untitledui/icons";
4
+ import { NavMenuItemLink } from "./base-components/nav-menu-item";
5
+
6
+ export const DropdownMenuSimple = () => {
7
+ const items = [
8
+ {
9
+ title: "Blog",
10
+ subtitle: "The latest industry new and guides curated by our expert team.",
11
+ href: "/blog",
12
+ Icon: BookClosed,
13
+ },
14
+ {
15
+ title: "Customer stories",
16
+ subtitle: "Learn how our customers are using Untitled UI to 10x their growth.",
17
+ href: "/customer-stories",
18
+ Icon: Stars02,
19
+ },
20
+ {
21
+ title: "Video tutorials",
22
+ subtitle: "Get up and running on our newest features and in-depth guides.",
23
+ href: "/tutorials",
24
+ Icon: PlayCircle,
25
+ },
26
+ {
27
+ title: "Documentation",
28
+ subtitle: "In-depth articles on our tools and technologies to empower teams.",
29
+ href: "/docs",
30
+ Icon: FileCode01,
31
+ },
32
+ {
33
+ title: "Help and support",
34
+ subtitle: "Need help with something? Our expert team is here to help 24/7.",
35
+ href: "/help",
36
+ Icon: LifeBuoy01,
37
+ },
38
+ ];
39
+
40
+ return (
41
+ <div className="px-3 pb-2 md:max-w-[336px] md:p-0">
42
+ <nav className="overflow-hidden rounded-2xl bg-primary py-2 shadow-xs ring-1 ring-border-secondary md:p-2 md:shadow-lg">
43
+ <ul className="flex flex-col gap-0.5">
44
+ {items.map(({ title, subtitle, href, Icon }) => (
45
+ <li>
46
+ <NavMenuItemLink icon={Icon} title={title} subtitle={subtitle} href={href} />
47
+ </li>
48
+ ))}
49
+ </ul>
50
+ </nav>
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { type ReactNode } from "react";
4
+ import { cx } from "@/components/utils";
5
+ import Avatar, { type AvatarProps } from "./avatar";
6
+
7
+ const styles = {
8
+ sm: { root: "gap-2", title: "tt-sm-semi", subtitle: "tt-xs" },
9
+ md: { root: "gap-2", title: "tt-sm-semi", subtitle: "tt-sm" },
10
+ lg: { root: "gap-3", title: "tt-md-semi", subtitle: "tt-md" },
11
+ xl: { root: "gap-4", title: "tt-lg-semi", subtitle: "tt-md" },
12
+ };
13
+
14
+ interface AvatarLabelGroupProps extends AvatarProps {
15
+ size: "sm" | "md" | "lg" | "xl";
16
+ title: string | ReactNode;
17
+ subtitle: string | ReactNode;
18
+ }
19
+
20
+ const AvatarLabelGroup = ({ title, subtitle, className, ...props }: AvatarLabelGroupProps) => {
21
+ return (
22
+ <div className={cx("group flex min-w-0 flex-1 items-center", styles[props.size].root, className)}>
23
+ <Avatar {...props} />
24
+ <div className="min-w-0 flex-1">
25
+ <p className={cx("text-primary", styles[props.size].title)}>{title}</p>
26
+ <p className={cx("truncate text-tertiary", styles[props.size].subtitle)}>{subtitle}</p>
27
+ </div>
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export default AvatarLabelGroup;
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { User01 } from "@untitledui/icons";
5
+ import { cx } from "@/components/utils";
6
+ import { type AvatarProps } from "./avatar";
7
+ import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
8
+
9
+ const styles = {
10
+ sm: { root: "size-18", icon: "size-9", initials: "td-sm-semi", ring: "ring-3 shadow-md", status: "bottom-0.5 right-0.5", tick: "bottom-0 right-0" },
11
+ md: { root: "size-24", icon: "size-12", initials: "td-md-semi", ring: "ring-4 shadow-lg", status: "bottom-1 right-1", tick: "bottom-0.5 right-0.5" },
12
+ lg: { root: "size-40", icon: "size-20", initials: "td-xl-semi", ring: "ring-4 shadow-lg", status: "bottom-3 right-3", tick: "bottom-1 right-1" },
13
+ };
14
+
15
+ const tickSizeMap = {
16
+ sm: "2xl",
17
+ md: "3xl",
18
+ lg: "4xl",
19
+ } as const;
20
+
21
+ interface AvatarProfilePhotoProps extends AvatarProps {
22
+ size: "sm" | "md" | "lg";
23
+ }
24
+
25
+ const AvatarProfilePhoto = ({
26
+ contrastBorder = true,
27
+ size = "md",
28
+ src,
29
+ alt,
30
+ initials,
31
+ placeholder,
32
+ placeholderIcon: PlaceholderIcon,
33
+ verified,
34
+ badge,
35
+ status,
36
+ className,
37
+ }: AvatarProfilePhotoProps) => {
38
+ const [isFailed, setIsFailed] = useState(false);
39
+
40
+ const renderMainContent = () => {
41
+ if (src && !isFailed) {
42
+ return <img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
43
+ }
44
+
45
+ if (initials) {
46
+ return <span className={cx("text-tertiary", styles[size].initials)}>{initials}</span>;
47
+ }
48
+
49
+ if (PlaceholderIcon) {
50
+ return <PlaceholderIcon className={cx("text-utility-gray-500", styles[size].icon)} />;
51
+ }
52
+
53
+ return placeholder || <User01 className={cx("text-utility-gray-500", styles[size].icon)} />;
54
+ };
55
+
56
+ const renderBadgeContent = () => {
57
+ if (status) {
58
+ return <AvatarOnlineIndicator status={status} size={tickSizeMap[size]} className={styles[size].status} />;
59
+ }
60
+
61
+ if (verified) {
62
+ return <VerifiedTick size={tickSizeMap[size]} className={cx("absolute", styles[size].tick)} />;
63
+ }
64
+
65
+ return badge;
66
+ };
67
+
68
+ return (
69
+ <div
70
+ className={cx(
71
+ "relative inline-flex shrink-0 items-center justify-center rounded-full bg-avatar-bg",
72
+ contrastBorder && "outline-1 -outline-offset-1 outline-avatar-contrast-border",
73
+ styles[size].root,
74
+ className,
75
+ )}
76
+ >
77
+ {renderMainContent()}
78
+ {renderBadgeContent()}
79
+ <div className={cx("absolute inset-0 rounded-full ring-avatar-profile-photo-border", styles[size].ring)}></div>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ export default AvatarProfilePhoto;
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { type FC, type ReactNode, useState } from "react";
4
+ import { User01 } from "@untitledui/icons";
5
+ import { cx } from "@/components/utils";
6
+ import { AvatarOnlineIndicator, VerifiedTick } from "./base-components";
7
+
8
+ type AvatarSize = "xxs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
9
+
10
+ export interface AvatarProps {
11
+ size?: AvatarSize;
12
+ className?: string;
13
+ src?: string | null;
14
+ alt?: string;
15
+ /**
16
+ * Display a contrast border around the avatar.
17
+ */
18
+ contrastBorder?: boolean;
19
+ /**
20
+ * Display a badge (i.e. company logo).
21
+ */
22
+ badge?: ReactNode;
23
+ /**
24
+ * Display a status indicator.
25
+ */
26
+ status?: "online" | "offline";
27
+ /**
28
+ * Display a verified tick icon.
29
+ *
30
+ * @default false
31
+ */
32
+ verified?: boolean;
33
+
34
+ /**
35
+ * The initials of the user to display if no image is available.
36
+ */
37
+ initials?: string;
38
+ /**
39
+ * An icon to display if no image is available.
40
+ */
41
+ placeholderIcon?: FC<{ className?: string }>;
42
+ /**
43
+ * A placeholder to display if no image is available.
44
+ */
45
+ placeholder?: ReactNode;
46
+
47
+ /**
48
+ * Whether the avatar should show a focus ring when the parent group is in focus.
49
+ * For example, when the avatar is wrapped inside a link.
50
+ *
51
+ * @default false
52
+ */
53
+ focusable?: boolean;
54
+ }
55
+
56
+ const styles = {
57
+ xxs: { root: "size-4 outline-[0.5px] -outline-offset-[0.5px]", initials: "tt-xs-semi", icon: "size-3" },
58
+ xs: { root: "size-6 outline-[0.5px] -outline-offset-[0.5px]", initials: "tt-xs-semi", icon: "size-4" },
59
+ sm: { root: "size-8 outline-[0.5px] -outline-offset-[0.5px]", initials: "tt-sm-semi", icon: "size-5" },
60
+ md: { root: "size-10 outline-[0.75px] -outline-offset-[0.75px]", initials: "tt-md-semi", icon: "size-6" },
61
+ lg: { root: "size-12 outline-[0.75px] -outline-offset-[0.75px]", initials: "tt-lg-semi", icon: "size-7" },
62
+ xl: { root: "size-14 outline-[0.75px] -outline-offset-[0.75px]", initials: "tt-xl-semi", icon: "size-8" },
63
+ "2xl": { root: "size-16 outline-[0.75px] -outline-offset-[0.75px]", initials: "td-xs-semi", icon: "size-8" },
64
+ };
65
+
66
+ const Avatar = ({
67
+ contrastBorder = true,
68
+ size = "md",
69
+ src,
70
+ alt,
71
+ initials,
72
+ placeholder,
73
+ placeholderIcon: PlaceholderIcon,
74
+ badge,
75
+ status,
76
+ verified,
77
+ focusable = false,
78
+ className,
79
+ }: AvatarProps) => {
80
+ const [isFailed, setIsFailed] = useState(false);
81
+
82
+ const renderMainContent = () => {
83
+ if (src && !isFailed) {
84
+ return <img className="size-full rounded-full object-cover" src={src} alt={alt} onError={() => setIsFailed(true)} />;
85
+ }
86
+
87
+ if (initials) {
88
+ return <span className={cx("text-tertiary", styles[size].initials)}>{initials}</span>;
89
+ }
90
+
91
+ if (PlaceholderIcon) {
92
+ return <PlaceholderIcon className={cx("text-utility-gray-500", styles[size].icon)} />;
93
+ }
94
+
95
+ return placeholder || <User01 className={cx("text-utility-gray-500", styles[size].icon)} />;
96
+ };
97
+ const renderBadgeContent = () => {
98
+ if (status) {
99
+ return <AvatarOnlineIndicator status={status} size={size === "xxs" ? "xs" : size} />;
100
+ }
101
+
102
+ if (verified) {
103
+ return (
104
+ <VerifiedTick
105
+ size={size === "xxs" ? "xs" : size}
106
+ className={cx("absolute right-0 bottom-0", (size === "xxs" || size === "xs") && "-right-px -bottom-px")}
107
+ />
108
+ );
109
+ }
110
+
111
+ return badge;
112
+ };
113
+
114
+ return (
115
+ <div
116
+ className={cx(
117
+ "relative inline-flex shrink-0 items-center justify-center rounded-full bg-avatar-bg outline-transparent",
118
+ // Focus styles
119
+ focusable && "group-outline-focus-ring group-focus:outline-2 group-focus:outline-offset-2",
120
+ contrastBorder && "outline outline-avatar-contrast-border",
121
+ styles[size].root,
122
+ className,
123
+ )}
124
+ >
125
+ {renderMainContent()}
126
+ {renderBadgeContent()}
127
+ </div>
128
+ );
129
+ };
130
+
131
+ export default Avatar;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { Plus } from "@untitledui/icons";
4
+ import type { ButtonProps } from "react-aria-components";
5
+ import { cx } from "@/components/utils";
6
+ import { Tooltip, TooltipTrigger } from "../../tooltips/tooltips";
7
+
8
+ const sizes = {
9
+ xs: "size-6",
10
+ sm: "size-8",
11
+ md: "size-10",
12
+ };
13
+
14
+ interface AvatarAddButtonProps extends ButtonProps {
15
+ size: "xs" | "sm" | "md";
16
+ title?: string;
17
+ className?: string;
18
+ }
19
+
20
+ export const AvatarAddButton = ({ size, className, title = "Add user", ...props }: AvatarAddButtonProps) => (
21
+ <Tooltip title={title}>
22
+ <TooltipTrigger
23
+ {...props}
24
+ className={cx(
25
+ "flex cursor-pointer items-center justify-center rounded-full border border-dashed border-primary bg-primary text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus:outline-2 focus:outline-offset-2 disabled:border-gray-200 disabled:bg-secondary disabled:text-gray-200",
26
+ sizes[size],
27
+ className,
28
+ )}
29
+ >
30
+ <Plus className="size-4 text-current transition-inherit-all" />
31
+ </TooltipTrigger>
32
+ </Tooltip>
33
+ );
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { cx } from "@/components/utils";
4
+
5
+ const sizes = {
6
+ xs: "size-2",
7
+ sm: "size-3",
8
+ md: "size-3.5",
9
+ lg: "size-4",
10
+ xl: "size-4.5",
11
+ "2xl": "size-5 ring-[1.67px]",
12
+ };
13
+
14
+ interface AvatarCompanyIconProps {
15
+ size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
16
+ src: string;
17
+ alt?: string;
18
+ }
19
+
20
+ export const AvatarCompanyIcon = ({ size, src, alt }: AvatarCompanyIconProps) => (
21
+ <img
22
+ src={src}
23
+ alt={alt}
24
+ className={cx("bg-primary-25 absolute -right-0.5 -bottom-0.5 rounded-full object-cover ring-[1.5px] ring-bg-primary", sizes[size])}
25
+ />
26
+ );
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { cx } from "@/components/utils";
4
+
5
+ const sizes = {
6
+ xs: "size-1.5",
7
+ sm: "size-2",
8
+ md: "size-2.5",
9
+ lg: "size-3",
10
+ xl: "size-3.5",
11
+ "2xl": "size-4",
12
+ "3xl": "size-4.5",
13
+ "4xl": "size-5",
14
+ };
15
+
16
+ interface AvatarOnlineIndicatorProps {
17
+ size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
18
+ status: "online" | "offline";
19
+ className?: string;
20
+ }
21
+
22
+ export const AvatarOnlineIndicator = ({ size, status, className }: AvatarOnlineIndicatorProps) => (
23
+ <span
24
+ className={cx(
25
+ "absolute right-0 bottom-0 z-10 rounded-full ring-[1.5px] ring-bg-primary",
26
+ status === "online" ? "bg-fg-success-secondary" : "bg-fg-disabled_subtle",
27
+ sizes[size],
28
+ className,
29
+ )}
30
+ />
31
+ );
@@ -0,0 +1,4 @@
1
+ export * from "./avatar-add-button";
2
+ export * from "./avatar-company-icon";
3
+ export * from "./avatar-online-indicator";
4
+ export * from "./verified-tick";