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,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
|
+
};
|
package/templates/default/src/components/marketing/header-navigation/dropdown-header-navigation.tsx
ADDED
|
@@ -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
|
+
);
|
package/templates/default/src/components/shared/avatar/base-components/avatar-company-icon.tsx
ADDED
|
@@ -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
|
+
);
|
package/templates/default/src/components/shared/avatar/base-components/avatar-online-indicator.tsx
ADDED
|
@@ -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
|
+
);
|