├── .eslintrc.json ├── app ├── favicon.ico ├── fonts │ └── GeistMono[wght].woff2 ├── layout.tsx └── page.tsx ├── next.config.mjs ├── postcss.config.mjs ├── global.d.ts ├── utils ├── tv.ts ├── cn.ts ├── polymorphic.ts └── recursive-clone-children.tsx ├── prettier.config.mjs ├── .vscode └── settings.json ├── components ├── ui │ ├── kbd.tsx │ ├── notification-provider.tsx │ ├── progress-bar.tsx │ ├── slider.tsx │ ├── label.tsx │ ├── divider.tsx │ ├── file-upload.tsx │ ├── digit-input.tsx │ ├── hint.tsx │ ├── file-format-icon.tsx │ ├── switch.tsx │ ├── dot-stepper.tsx │ ├── popover.tsx │ ├── breadcrumb.tsx │ ├── avatar-group.tsx │ ├── avatar-group-compact.tsx │ ├── segmented-control.tsx │ ├── tooltip.tsx │ ├── tab-menu-vertical.tsx │ ├── table.tsx │ ├── link-button.tsx │ ├── datepicker.tsx │ ├── progress-circle.tsx │ ├── radio.tsx │ ├── compact-button.tsx │ ├── status-badge.tsx │ ├── notification.tsx │ ├── button-group.tsx │ ├── fancy-button.tsx │ ├── drawer.tsx │ ├── vertical-stepper.tsx │ ├── accordion.tsx │ ├── horizontal-stepper.tsx │ ├── svg-rating-icons.tsx │ ├── color-picker.tsx │ ├── avatar-empty-icons.tsx │ ├── tab-menu-horizontal.tsx │ ├── tag.tsx │ ├── pagination.tsx │ ├── command-menu.tsx │ ├── modal.tsx │ ├── textarea.tsx │ ├── social-button.tsx │ ├── checkbox.tsx │ ├── dropdown.tsx │ └── alert.tsx ├── header.tsx └── theme-switch.tsx ├── .gitignore ├── tsconfig.json ├── README.md ├── LICENSE ├── hooks ├── use-tab-observer.ts └── use-notification.ts ├── package.json └── public └── images └── logo.svg /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alignui/alignui-nextjs-typescript-starter/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMono[wght].woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alignui/alignui-nextjs-typescript-starter/HEAD/app/fonts/GeistMono[wght].woff2 -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | /* config options here */ 4 | }; 5 | 6 | export default nextConfig; 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import * as React from 'react'; 3 | 4 | declare module 'react' { 5 | interface CSSProperties { 6 | [key: `--${string}`]: string | number; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /utils/tv.ts: -------------------------------------------------------------------------------- 1 | import { createTV } from 'tailwind-variants'; 2 | export type { VariantProps, ClassValue } from 'tailwind-variants'; 3 | 4 | import { twMergeConfig } from '@/utils/cn'; 5 | 6 | export const tv = createTV({ 7 | twMergeConfig, 8 | }); 9 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | tabWidth: 2, 6 | semi: true, 7 | printWidth: 80, 8 | plugins: ['prettier-plugin-tailwindcss'], 9 | tailwindFunctions: ['tv', 'cn'], 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.indentSize": 2, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "tailwindCSS.experimental.classRegex": [ 7 | ["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"] 8 | ], 9 | "tailwindCSS.classAttributes": ["class", "className", ".*ClassName"] 10 | } 11 | -------------------------------------------------------------------------------- /components/ui/kbd.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI Kbd v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { cn } from '@/utils/cn'; 5 | 6 | function Kbd({ className, ...rest }: React.HTMLAttributes) { 7 | return ( 8 |
15 | ); 16 | } 17 | 18 | export { Kbd as Root }; 19 | -------------------------------------------------------------------------------- /components/ui/notification-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useNotification } from '@/hooks/use-notification'; 4 | 5 | import * as Notification from '@/components/ui/notification'; 6 | 7 | const NotificationProvider = () => { 8 | const { notifications } = useNotification(); 9 | 10 | return ( 11 | 12 | {notifications.map(({ id, ...rest }) => { 13 | return ; 14 | })} 15 | 16 | 17 | ); 18 | }; 19 | 20 | export { NotificationProvider }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import dynamic from 'next/dynamic'; 3 | 4 | const DynamicThemeSwitch = dynamic(() => import('./theme-switch'), { 5 | ssr: false, 6 | }); 7 | 8 | export default function Header() { 9 | return ( 10 |
11 |
12 | 16 | {/* eslint-disable-next-line @next/next/no-img-element */} 17 | 22 | AlignUI 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /utils/cn.ts: -------------------------------------------------------------------------------- 1 | import clsx, { type ClassValue } from 'clsx'; 2 | export { type ClassValue } from 'clsx'; 3 | import { extendTailwindMerge } from 'tailwind-merge'; 4 | import { shadows, texts, borderRadii } from '@/tailwind.config'; 5 | 6 | export const twMergeConfig = { 7 | extend: { 8 | classGroups: { 9 | 'font-size': [ 10 | { 11 | text: Object.keys(texts), 12 | }, 13 | ], 14 | shadow: [ 15 | { 16 | shadow: Object.keys(shadows), 17 | }, 18 | ], 19 | rounded: [ 20 | { 21 | rounded: Object.keys(borderRadii), 22 | }, 23 | ], 24 | }, 25 | }, 26 | }; 27 | 28 | const customTwMerge = extendTailwindMerge(twMergeConfig); 29 | 30 | /** 31 | * Utilizes `clsx` with `tailwind-merge`, use in cases of possible class conflicts. 32 | */ 33 | export function cn(...classes: ClassValue[]) { 34 | return customTwMerge(clsx(...classes)); 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

AlignUI Design System

5 | 6 |

The Design System You Need

7 |

8 | 9 | [Join the AlignUI Community](https://discord.gg/alignui) 10 | 11 | # AlignUI Starter Template with Next.js 12 | 13 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 14 | 15 | ## Features 16 | 17 | - 🔸 Includes all styles 18 | - 🔸 Ready-to-use Tailwind setup 19 | - 🔸 All base components included 20 | - 🔸 All utils included 21 | - 🔸 Inter font setup 22 | - 🔸 Dark mode toggle included 23 | 24 | ## Getting Started 25 | 26 | **Install dependencies** 27 | 28 | ```bash 29 | pnpm i 30 | ``` 31 | 32 | **Run the development server:** 33 | 34 | ```bash 35 | pnpm dev 36 | ``` 37 | 38 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 39 | -------------------------------------------------------------------------------- /components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import * as SegmentedControl from '@/components/ui/segmented-control'; 5 | import { RiEqualizer3Fill, RiMoonLine, RiSunLine } from '@remixicon/react'; 6 | 7 | export default function ThemeSwitch() { 8 | const { theme, setTheme } = useTheme(); 9 | 10 | return ( 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /utils/polymorphic.ts: -------------------------------------------------------------------------------- 1 | type AsProp = { 2 | as?: T; 3 | }; 4 | 5 | type PropsToOmit = keyof (AsProp & P); 6 | 7 | type PolymorphicComponentProp< 8 | T extends React.ElementType, 9 | Props = {}, 10 | > = React.PropsWithChildren> & 11 | Omit, PropsToOmit>; 12 | 13 | export type PolymorphicRef = 14 | React.ComponentPropsWithRef['ref']; 15 | 16 | type PolymorphicComponentPropWithRef< 17 | T extends React.ElementType, 18 | Props = {}, 19 | > = PolymorphicComponentProp & { ref?: PolymorphicRef }; 20 | 21 | export type PolymorphicComponentPropsWithRef< 22 | T extends React.ElementType, 23 | P = {}, 24 | > = PolymorphicComponentPropWithRef; 25 | 26 | export type PolymorphicComponentProps< 27 | T extends React.ElementType, 28 | P = {}, 29 | > = PolymorphicComponentProp; 30 | 31 | export type PolymorphicComponent

= { 32 | ( 33 | props: PolymorphicComponentPropsWithRef, 34 | ): React.ReactNode; 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Doğukan Çavuş 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter as FontSans } from 'next/font/google'; 3 | import localFont from 'next/font/local'; 4 | import './globals.css'; 5 | import { ThemeProvider } from 'next-themes'; 6 | import { cn } from '@/utils/cn'; 7 | import { Provider as TooltipProvider } from '@/components/ui/tooltip'; 8 | import { NotificationProvider } from '@/components/ui/notification-provider'; 9 | import Header from '@/components/header'; 10 | 11 | const inter = FontSans({ 12 | subsets: ['latin'], 13 | variable: '--font-sans', 14 | }); 15 | 16 | const geistMono = localFont({ 17 | src: './fonts/GeistMono[wght].woff2', 18 | variable: '--font-geist-mono', 19 | weight: '100 900', 20 | }); 21 | 22 | export const metadata: Metadata = { 23 | title: 'Create Next App', 24 | description: 'Generated by create next app', 25 | }; 26 | 27 | export default function RootLayout({ 28 | children, 29 | }: Readonly<{ 30 | children: React.ReactNode; 31 | }>) { 32 | return ( 33 | 38 | 39 | 40 | 41 |

42 |
43 |
{children}
44 |
45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /hooks/use-tab-observer.ts: -------------------------------------------------------------------------------- 1 | // AlignUI useTabObserver v0.0.0 2 | 3 | import * as React from 'react'; 4 | 5 | interface TabObserverOptions { 6 | onActiveTabChange?: (index: number, element: HTMLElement) => void; 7 | } 8 | 9 | export function useTabObserver({ onActiveTabChange }: TabObserverOptions = {}) { 10 | const [mounted, setMounted] = React.useState(false); 11 | const listRef = React.useRef(null); 12 | const onActiveTabChangeRef = React.useRef(onActiveTabChange); 13 | 14 | React.useEffect(() => { 15 | onActiveTabChangeRef.current = onActiveTabChange; 16 | }, [onActiveTabChange]); 17 | 18 | const handleUpdate = React.useCallback(() => { 19 | if (listRef.current) { 20 | const tabs = listRef.current.querySelectorAll('[role="tab"]'); 21 | tabs.forEach((el, i) => { 22 | if (el.getAttribute('data-state') === 'active') { 23 | onActiveTabChangeRef.current?.(i, el as HTMLElement); 24 | } 25 | }); 26 | } 27 | }, []); 28 | 29 | React.useEffect(() => { 30 | setMounted(true); 31 | 32 | const resizeObserver = new ResizeObserver(handleUpdate); 33 | const mutationObserver = new MutationObserver(handleUpdate); 34 | 35 | if (listRef.current) { 36 | resizeObserver.observe(listRef.current); 37 | mutationObserver.observe(listRef.current, { 38 | childList: true, 39 | subtree: true, 40 | attributes: true, 41 | }); 42 | } 43 | 44 | handleUpdate(); 45 | 46 | return () => { 47 | resizeObserver.disconnect(); 48 | mutationObserver.disconnect(); 49 | }; 50 | }, []); 51 | 52 | return { mounted, listRef }; 53 | } 54 | -------------------------------------------------------------------------------- /components/ui/progress-bar.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI ProgressBar v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { tv, type VariantProps } from '@/utils/tv'; 5 | 6 | export const progressBarVariants = tv({ 7 | slots: { 8 | root: 'h-1.5 w-full rounded-full bg-bg-soft-200', 9 | progress: 'h-full rounded-full transition-all duration-300 ease-out', 10 | }, 11 | variants: { 12 | color: { 13 | blue: { 14 | progress: 'bg-information-base', 15 | }, 16 | red: { 17 | progress: 'bg-error-base', 18 | }, 19 | orange: { 20 | progress: 'bg-warning-base', 21 | }, 22 | green: { 23 | progress: 'bg-success-base', 24 | }, 25 | }, 26 | }, 27 | defaultVariants: { 28 | color: 'blue', 29 | }, 30 | }); 31 | 32 | type ProgressBarRootProps = React.HTMLAttributes & 33 | VariantProps & { 34 | value?: number; 35 | max?: number; 36 | }; 37 | 38 | const ProgressBarRoot = React.forwardRef( 39 | ({ className, color, value = 0, max = 100, ...rest }, forwardedRef) => { 40 | const { root, progress } = progressBarVariants({ color }); 41 | const safeValue = Math.min(max, Math.max(value, 0)); 42 | 43 | return ( 44 |
45 |
54 |
55 | ); 56 | }, 57 | ); 58 | ProgressBarRoot.displayName = 'ProgressBarRoot'; 59 | 60 | export { ProgressBarRoot as Root }; 61 | -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SliderPrimitive from '@radix-ui/react-slider'; 5 | 6 | import { cn } from '@/utils/cn'; 7 | 8 | const SLIDER_ROOT_NAME = 'SliderRoot'; 9 | const SLIDER_THUMB_NAME = 'SliderThumb'; 10 | 11 | const SliderRoot = React.forwardRef< 12 | React.ComponentRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, children, ...rest }, forwardedRef) => ( 15 | 23 | 24 | 25 | 26 | {children} 27 | 28 | )); 29 | SliderRoot.displayName = SLIDER_ROOT_NAME; 30 | 31 | const SliderThumb = React.forwardRef< 32 | React.ComponentRef, 33 | React.ComponentPropsWithoutRef 34 | >(({ className, ...rest }, forwardedRef) => { 35 | return ( 36 | 49 | ); 50 | }); 51 | SliderThumb.displayName = SLIDER_THUMB_NAME; 52 | 53 | export { SliderRoot as Root, SliderThumb as Thumb }; 54 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI Label v0.0.0 2 | 3 | 'use client'; 4 | 5 | import * as React from 'react'; 6 | import * as LabelPrimitives from '@radix-ui/react-label'; 7 | import { cn } from '@/utils/cn'; 8 | 9 | const LabelRoot = React.forwardRef< 10 | React.ComponentRef, 11 | React.ComponentPropsWithoutRef & { 12 | disabled?: boolean; 13 | } 14 | >(({ className, disabled, ...rest }, forwardedRef) => { 15 | return ( 16 | 28 | ); 29 | }); 30 | LabelRoot.displayName = 'LabelRoot'; 31 | 32 | function LabelAsterisk({ 33 | className, 34 | children, 35 | ...rest 36 | }: React.HTMLAttributes) { 37 | return ( 38 | 47 | {children || '*'} 48 | 49 | ); 50 | } 51 | 52 | function LabelSub({ 53 | children, 54 | className, 55 | ...rest 56 | }: React.HTMLAttributes) { 57 | return ( 58 | 67 | {children} 68 | 69 | ); 70 | } 71 | 72 | export { LabelRoot as Root, LabelAsterisk as Asterisk, LabelSub as Sub }; 73 | -------------------------------------------------------------------------------- /utils/recursive-clone-children.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as React from 'react'; 3 | 4 | /** 5 | * Recursively clones React children, adding additional props to components with matched display names. 6 | * 7 | * @param children - The node(s) to be cloned. 8 | * @param additionalProps - The props to add to the matched components. 9 | * @param displayNames - An array of display names to match components against. 10 | * @param asChild - Indicates whether the parent component uses the Slot component. 11 | * @param uniqueId - A unique ID prefix from the parent component to generate stable keys. 12 | * 13 | * @returns The cloned node(s) with the additional props applied to the matched components. 14 | */ 15 | export function recursiveCloneChildren( 16 | children: React.ReactNode, 17 | additionalProps: any, 18 | displayNames: string[], 19 | uniqueId: string, 20 | asChild?: boolean, 21 | ): React.ReactNode | React.ReactNode[] { 22 | const mappedChildren = React.Children.map( 23 | children, 24 | (child: React.ReactNode, index) => { 25 | if (!React.isValidElement(child)) { 26 | return child; 27 | } 28 | 29 | const displayName = 30 | (child.type as React.ComponentType)?.displayName || ''; 31 | const newProps = displayNames.includes(displayName) 32 | ? additionalProps 33 | : {}; 34 | 35 | const childProps = (child as React.ReactElement).props; 36 | 37 | return React.cloneElement( 38 | child, 39 | { ...newProps, key: `${uniqueId}-${index}` }, 40 | recursiveCloneChildren( 41 | childProps?.children, 42 | additionalProps, 43 | displayNames, 44 | uniqueId, 45 | childProps?.asChild, 46 | ), 47 | ); 48 | }, 49 | ); 50 | 51 | return asChild ? mappedChildren?.[0] : mappedChildren; 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alignui-nextjs-typescript-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-accordion": "^1.2.1", 14 | "@radix-ui/react-checkbox": "^1.1.2", 15 | "@radix-ui/react-dialog": "^1.1.2", 16 | "@radix-ui/react-dropdown-menu": "^2.1.2", 17 | "@radix-ui/react-label": "^2.1.0", 18 | "@radix-ui/react-popover": "^1.1.2", 19 | "@radix-ui/react-radio-group": "^1.2.1", 20 | "@radix-ui/react-scroll-area": "^1.2.0", 21 | "@radix-ui/react-select": "^2.1.2", 22 | "@radix-ui/react-slider": "^1.2.1", 23 | "@radix-ui/react-slot": "^1.1.0", 24 | "@radix-ui/react-switch": "^1.1.1", 25 | "@radix-ui/react-tabs": "^1.1.1", 26 | "@radix-ui/react-toast": "^1.2.2", 27 | "@radix-ui/react-tooltip": "^1.1.3", 28 | "@remixicon/react": "^4.5.0", 29 | "clsx": "^2.1.1", 30 | "cmdk": "^1.0.4", 31 | "date-fns": "^3", 32 | "merge-refs": "^1.3.0", 33 | "next": "14.2.16", 34 | "next-themes": "^0.4.3", 35 | "react": "18.3.1", 36 | "react-aria-components": "^1.4.1", 37 | "react-day-picker": "8.10.1", 38 | "react-dom": "18.3.1", 39 | "react-otp-input": "^3.1.1", 40 | "sonner": "^1.7.0", 41 | "tailwind-merge": "^2.5.4", 42 | "tailwind-variants": "^0.2.1" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^20", 46 | "@types/react": "^18", 47 | "@types/react-dom": "^18", 48 | "eslint": "^8", 49 | "eslint-config-next": "15.0.2", 50 | "postcss": "^8", 51 | "prettier": "^3.3.3", 52 | "prettier-plugin-tailwindcss": "^0.6.8", 53 | "tailwindcss": "^3.4.14", 54 | "tailwindcss-animate": "^1.0.7", 55 | "typescript": "^5" 56 | } 57 | } -------------------------------------------------------------------------------- /components/ui/divider.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI Divider v0.0.0 2 | 3 | import { tv, type VariantProps } from '@/utils/tv'; 4 | 5 | const DIVIDER_ROOT_NAME = 'DividerRoot'; 6 | 7 | export const dividerVariants = tv({ 8 | base: 'relative flex w-full items-center', 9 | variants: { 10 | variant: { 11 | line: 'h-0 before:absolute before:left-0 before:top-1/2 before:h-px before:w-full before:-translate-y-1/2 before:bg-stroke-soft-200', 12 | 'line-spacing': [ 13 | // base 14 | 'h-1', 15 | // before 16 | 'before:absolute before:left-0 before:top-1/2 before:h-px before:w-full before:-translate-y-1/2 before:bg-stroke-soft-200', 17 | ], 18 | 'line-text': [ 19 | // base 20 | 'gap-2.5', 21 | 'text-subheading-2xs text-text-soft-400', 22 | // before 23 | 'before:h-px before:w-full before:flex-1 before:bg-stroke-soft-200', 24 | // after 25 | 'after:h-px after:w-full after:flex-1 after:bg-stroke-soft-200', 26 | ], 27 | content: [ 28 | // base 29 | 'gap-2.5', 30 | // before 31 | 'before:h-px before:w-full before:flex-1 before:bg-stroke-soft-200', 32 | // after 33 | 'after:h-px after:w-full after:flex-1 after:bg-stroke-soft-200', 34 | ], 35 | text: [ 36 | // base 37 | 'px-2 py-1', 38 | 'text-subheading-xs text-text-soft-400', 39 | ], 40 | 'solid-text': [ 41 | // base 42 | 'bg-bg-weak-50 px-5 py-1.5 uppercase', 43 | 'text-subheading-xs text-text-soft-400', 44 | ], 45 | }, 46 | }, 47 | defaultVariants: { 48 | variant: 'line', 49 | }, 50 | }); 51 | 52 | function Divider({ 53 | className, 54 | variant, 55 | ...rest 56 | }: React.HTMLAttributes & 57 | VariantProps) { 58 | return ( 59 |
64 | ); 65 | } 66 | Divider.displayName = DIVIDER_ROOT_NAME; 67 | 68 | export { Divider as Root }; 69 | -------------------------------------------------------------------------------- /components/ui/file-upload.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI FileUpload v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { cn } from '@/utils/cn'; 5 | import { PolymorphicComponentProps } from '@/utils/polymorphic'; 6 | import { Slot } from '@radix-ui/react-slot'; 7 | 8 | const FileUpload = React.forwardRef< 9 | HTMLLabelElement, 10 | React.LabelHTMLAttributes & { 11 | asChild?: boolean; 12 | } 13 | >(({ className, asChild, ...rest }, forwardedRef) => { 14 | const Component = asChild ? Slot : 'label'; 15 | 16 | return ( 17 | 28 | ); 29 | }); 30 | FileUpload.displayName = 'FileUpload'; 31 | 32 | const FileUploadButton = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes & { 35 | asChild?: boolean; 36 | } 37 | >(({ className, asChild, ...rest }, forwardedRef) => { 38 | const Component = asChild ? Slot : 'div'; 39 | 40 | return ( 41 | 50 | ); 51 | }); 52 | FileUploadButton.displayName = 'FileUploadButton'; 53 | 54 | function FileUploadIcon({ 55 | className, 56 | as, 57 | ...rest 58 | }: PolymorphicComponentProps) { 59 | const Component = as || 'div'; 60 | 61 | return ( 62 | 66 | ); 67 | } 68 | 69 | export { 70 | FileUpload as Root, 71 | FileUploadButton as Button, 72 | FileUploadIcon as Icon, 73 | }; 74 | -------------------------------------------------------------------------------- /components/ui/digit-input.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI DigitInput v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { cn } from '@/utils/cn'; 5 | 6 | import OtpInput, { OTPInputProps } from 'react-otp-input'; 7 | 8 | type OtpOptions = Omit; 9 | 10 | type DigitInputProps = { 11 | className?: string; 12 | disabled?: boolean; 13 | hasError?: boolean; 14 | } & OtpOptions; 15 | 16 | function DigitInput({ 17 | className, 18 | disabled, 19 | hasError, 20 | ...rest 21 | }: DigitInputProps) { 22 | return ( 23 | ( 27 | 32 | )} 33 | {...rest} 34 | /> 35 | ); 36 | } 37 | DigitInput.displayName = 'DigitInput'; 38 | 39 | const DigitInputSlot = React.forwardRef< 40 | React.ComponentRef<'input'>, 41 | React.ComponentPropsWithoutRef<'input'> & { 42 | hasError?: boolean; 43 | } 44 | >(({ className, hasError, ...rest }, forwardedRef) => { 45 | return ( 46 | 67 | ); 68 | }); 69 | DigitInputSlot.displayName = 'DigitInputSlot'; 70 | 71 | export { DigitInput as Root }; 72 | -------------------------------------------------------------------------------- /components/ui/hint.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI Hint v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { tv, type VariantProps } from '@/utils/tv'; 5 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children'; 6 | import { PolymorphicComponentProps } from '@/utils/polymorphic'; 7 | 8 | const HINT_ROOT_NAME = 'HintRoot'; 9 | const HINT_ICON_NAME = 'HintIcon'; 10 | 11 | export const hintVariants = tv({ 12 | slots: { 13 | root: 'group flex items-center gap-1 text-paragraph-xs text-text-sub-600', 14 | icon: 'size-4 shrink-0 text-text-soft-400', 15 | }, 16 | variants: { 17 | disabled: { 18 | true: { 19 | root: 'text-text-disabled-300', 20 | icon: 'text-text-disabled-300', 21 | }, 22 | }, 23 | hasError: { 24 | true: { 25 | root: 'text-error-base', 26 | icon: 'text-error-base', 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | type HintSharedProps = VariantProps; 33 | 34 | type HintRootProps = VariantProps & 35 | React.HTMLAttributes; 36 | 37 | function HintRoot({ 38 | children, 39 | hasError, 40 | disabled, 41 | className, 42 | ...rest 43 | }: HintRootProps) { 44 | const uniqueId = React.useId(); 45 | const { root } = hintVariants({ hasError, disabled }); 46 | 47 | const sharedProps: HintSharedProps = { 48 | hasError, 49 | disabled, 50 | }; 51 | 52 | const extendedChildren = recursiveCloneChildren( 53 | children as React.ReactElement[], 54 | sharedProps, 55 | [HINT_ICON_NAME], 56 | uniqueId, 57 | ); 58 | 59 | return ( 60 |
61 | {extendedChildren} 62 |
63 | ); 64 | } 65 | HintRoot.displayName = HINT_ROOT_NAME; 66 | 67 | function HintIcon({ 68 | as, 69 | className, 70 | hasError, 71 | disabled, 72 | ...rest 73 | }: PolymorphicComponentProps) { 74 | const Component = as || 'div'; 75 | const { icon } = hintVariants({ hasError, disabled }); 76 | 77 | return ; 78 | } 79 | HintIcon.displayName = HINT_ICON_NAME; 80 | 81 | export { HintRoot as Root, HintIcon as Icon }; 82 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import * as Button from '@/components/ui/button'; 3 | import { RiGithubFill } from '@remixicon/react'; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 |

10 | Quick Starter AlignUI Template with Next.js & Typescript 11 |

12 | 13 |
14 | 15 | 19 | 20 | Give a star 21 | 22 | 23 | 24 | 25 | 26 | Read our docs 27 | 28 | 29 |
30 | 31 |
32 |

33 | What's Included: 34 |

35 |
    36 |
  • Tailwind setup with AlignUI tokens.
  • 37 |
  • 38 | Base components are stored in{' '} 39 | 40 | /components/ui 41 | {' '} 42 | folder. 43 |
  • 44 |
  • 45 | Utils are stored in{' '} 46 | 47 | /utils 48 | {' '} 49 | folder. 50 |
  • 51 |
  • 52 | Custom hooks are stored in{' '} 53 | 54 | /hooks 55 | {' '} 56 | folder. 57 |
  • 58 |
  • Dark mode setup.
  • 59 |
  • Inter font setup.
  • 60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/ui/file-format-icon.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI FileFormatIcon v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { tv, type VariantProps } from '@/utils/tv'; 5 | 6 | export const fileFormatIconVariants = tv({ 7 | slots: { 8 | root: 'relative shrink-0', 9 | formatBox: 10 | 'absolute bottom-1.5 left-0 flex h-4 items-center rounded px-[3px] py-0.5 text-[11px] font-semibold leading-none text-static-white', 11 | }, 12 | variants: { 13 | size: { 14 | medium: { 15 | root: 'size-10', 16 | }, 17 | small: { 18 | root: 'size-8', 19 | }, 20 | }, 21 | color: { 22 | red: { 23 | formatBox: 'bg-error-base', 24 | }, 25 | orange: { 26 | formatBox: 'bg-warning-base', 27 | }, 28 | yellow: { 29 | formatBox: 'bg-away-base', 30 | }, 31 | green: { 32 | formatBox: 'bg-success-base', 33 | }, 34 | sky: { 35 | formatBox: 'bg-verified-base', 36 | }, 37 | blue: { 38 | formatBox: 'bg-information-base', 39 | }, 40 | purple: { 41 | formatBox: 'bg-feature-base', 42 | }, 43 | pink: { 44 | formatBox: 'bg-highlighted-base', 45 | }, 46 | gray: { 47 | formatBox: 'bg-faded-base', 48 | }, 49 | }, 50 | }, 51 | defaultVariants: { 52 | color: 'gray', 53 | size: 'medium', 54 | }, 55 | }); 56 | 57 | function FileFormatIcon({ 58 | format, 59 | className, 60 | color, 61 | size, 62 | ...rest 63 | }: VariantProps & 64 | React.SVGProps) { 65 | const { root, formatBox } = fileFormatIconVariants({ color, size }); 66 | 67 | return ( 68 | 77 | 82 | 87 | 88 | {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} 89 | {/* @ts-ignore */} 90 |
91 | {format} 92 |
93 |
94 |
95 | ); 96 | } 97 | 98 | export { FileFormatIcon as Root }; 99 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 3 | import { cn } from '@/utils/cn'; 4 | 5 | const Switch = React.forwardRef< 6 | React.ComponentRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, disabled, ...rest }, forwardedRef) => { 9 | return ( 10 | 19 |
46 | 70 |
71 |
72 | ); 73 | }); 74 | Switch.displayName = SwitchPrimitives.Root.displayName; 75 | 76 | export { Switch as Root }; 77 | -------------------------------------------------------------------------------- /components/ui/dot-stepper.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI DotStepper v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { tv, type VariantProps } from '@/utils/tv'; 5 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children'; 6 | import { cn } from '@/utils/cn'; 7 | import { Slot } from '@radix-ui/react-slot'; 8 | 9 | const DOT_STEPPER_ROOT_NAME = 'DotStepperRoot'; 10 | const DOT_STEPPER_ITEM_NAME = 'DotStepperItem'; 11 | 12 | export const dotStepperVariants = tv({ 13 | slots: { 14 | root: 'flex flex-wrap', 15 | item: [ 16 | // base 17 | 'shrink-0 rounded-full bg-bg-soft-200 outline-none transition duration-200 ease-out', 18 | // focus 19 | 'focus:outline-none', 20 | 'focus-visible:ring-2 focus-visible:ring-stroke-strong-950', 21 | ], 22 | }, 23 | variants: { 24 | size: { 25 | small: { 26 | root: 'gap-2.5', 27 | item: 'size-2', 28 | }, 29 | xsmall: { 30 | root: 'gap-1.5', 31 | item: 'size-1', 32 | }, 33 | }, 34 | }, 35 | defaultVariants: { 36 | size: 'small', 37 | }, 38 | }); 39 | 40 | type DotStepperSharedProps = VariantProps; 41 | 42 | type DotStepperRootProps = React.HTMLAttributes & 43 | VariantProps & { 44 | asChild?: boolean; 45 | }; 46 | 47 | function DotStepperRoot({ 48 | asChild, 49 | children, 50 | size, 51 | className, 52 | ...rest 53 | }: DotStepperRootProps) { 54 | const uniqueId = React.useId(); 55 | const Component = asChild ? Slot : 'div'; 56 | const { root } = dotStepperVariants({ size }); 57 | 58 | const sharedProps: DotStepperSharedProps = { 59 | size, 60 | }; 61 | 62 | const extendedChildren = recursiveCloneChildren( 63 | children as React.ReactElement[], 64 | sharedProps, 65 | [DOT_STEPPER_ITEM_NAME], 66 | uniqueId, 67 | asChild, 68 | ); 69 | 70 | return ( 71 | 72 | {extendedChildren} 73 | 74 | ); 75 | } 76 | DotStepperRoot.displayName = DOT_STEPPER_ROOT_NAME; 77 | 78 | type DotStepperItemProps = React.ButtonHTMLAttributes & 79 | DotStepperSharedProps & { 80 | asChild?: boolean; 81 | active?: boolean; 82 | }; 83 | 84 | const DotStepperItem = React.forwardRef( 85 | ({ asChild, size, className, active, ...rest }, forwardedRef) => { 86 | const Component = asChild ? Slot : 'button'; 87 | const { item } = dotStepperVariants({ size }); 88 | 89 | return ( 90 | 97 | ); 98 | }, 99 | ); 100 | DotStepperItem.displayName = DOT_STEPPER_ITEM_NAME; 101 | 102 | export { DotStepperRoot as Root, DotStepperItem as Item }; 103 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI Popover v0.0.0 2 | 3 | import * as React from 'react'; 4 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 5 | import { Slottable } from '@radix-ui/react-slot'; 6 | 7 | import { cn } from '@/utils/cn'; 8 | 9 | const PopoverRoot = PopoverPrimitive.Root; 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | const PopoverAnchor = PopoverPrimitive.Anchor; 12 | 13 | const PopoverContent = React.forwardRef< 14 | React.ComponentRef, 15 | React.ComponentPropsWithoutRef & { 16 | showArrow?: boolean; 17 | unstyled?: boolean; 18 | } 19 | >( 20 | ( 21 | { 22 | children, 23 | className, 24 | align = 'center', 25 | sideOffset = 12, 26 | collisionPadding = 12, 27 | arrowPadding = 12, 28 | showArrow = true, 29 | unstyled, 30 | ...rest 31 | }, 32 | forwardedRef, 33 | ) => ( 34 | 35 | 56 | {children} 57 | {showArrow && ( 58 | 59 |
60 |
61 | )} 62 |
63 |
64 | ), 65 | ); 66 | PopoverContent.displayName = 'PopoverContent'; 67 | 68 | const PopoverClose = React.forwardRef< 69 | React.ComponentRef, 70 | React.ComponentPropsWithoutRef & { 71 | unstyled?: boolean; 72 | } 73 | >(({ className, unstyled, ...rest }, forwardedRef) => ( 74 | 79 | )); 80 | PopoverClose.displayName = 'PopoverClose'; 81 | 82 | export { 83 | PopoverRoot as Root, 84 | PopoverAnchor as Anchor, 85 | PopoverTrigger as Trigger, 86 | PopoverContent as Content, 87 | PopoverClose as Close, 88 | }; 89 | -------------------------------------------------------------------------------- /components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI Breadcrumb v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { Slot } from '@radix-ui/react-slot'; 5 | import { cn } from '@/utils/cn'; 6 | import { PolymorphicComponentProps } from '@/utils/polymorphic'; 7 | 8 | const BREADCRUMB_ROOT_NAME = 'BreadcrumbRoot'; 9 | const BREADCRUMB_ITEM_NAME = 'BreadcrumbItem'; 10 | const BREADCRUMB_ICON_NAME = 'BreadcrumbIcon'; 11 | const BREADCRUMB_ARROW_NAME = 'BreadcrumbArrow'; 12 | 13 | type BreadcrumbRootProps = React.HTMLAttributes & { 14 | asChild?: boolean; 15 | }; 16 | 17 | function BreadcrumbRoot({ 18 | asChild, 19 | children, 20 | className, 21 | ...rest 22 | }: BreadcrumbRootProps) { 23 | const Component = asChild ? Slot : 'div'; 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | BreadcrumbRoot.displayName = BREADCRUMB_ROOT_NAME; 32 | 33 | type BreadcrumbItemProps = React.HTMLAttributes & { 34 | asChild?: boolean; 35 | active?: boolean; 36 | }; 37 | 38 | const BreadcrumbItem = React.forwardRef( 39 | ({ asChild, children, className, active, ...rest }, forwardedRef) => { 40 | const Component = asChild ? Slot : 'div'; 41 | 42 | return ( 43 | 61 | {children} 62 | 63 | ); 64 | }, 65 | ); 66 | BreadcrumbItem.displayName = BREADCRUMB_ITEM_NAME; 67 | 68 | function BreadcrumbItemIcon({ 69 | className, 70 | as, 71 | ...rest 72 | }: PolymorphicComponentProps) { 73 | const Component = as || 'div'; 74 | 75 | return ; 76 | } 77 | BreadcrumbItemIcon.displayName = BREADCRUMB_ICON_NAME; 78 | 79 | function BreadcrumbItemArrowIcon({ 80 | className, 81 | as, 82 | ...rest 83 | }: PolymorphicComponentProps) { 84 | const Component = as || 'div'; 85 | 86 | return ( 87 | 94 | ); 95 | } 96 | BreadcrumbItemArrowIcon.displayName = BREADCRUMB_ARROW_NAME; 97 | 98 | export { 99 | BreadcrumbRoot as Root, 100 | BreadcrumbItem as Item, 101 | BreadcrumbItemIcon as Icon, 102 | BreadcrumbItemArrowIcon as ArrowIcon, 103 | }; 104 | -------------------------------------------------------------------------------- /components/ui/avatar-group.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI AvatarGroup v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { tv, type VariantProps } from '@/utils/tv'; 5 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children'; 6 | import { AVATAR_ROOT_NAME } from './avatar'; 7 | 8 | const AVATAR_GROUP_ROOT_NAME = 'AvatarGroupRoot'; 9 | const AVATAR_GROUP_OVERFLOW_NAME = 'AvatarGroupOverflow'; 10 | 11 | export const avatarGroupVariants = tv({ 12 | slots: { 13 | root: 'flex *:ring-2 *:ring-stroke-white-0', 14 | overflow: 15 | 'relative flex shrink-0 items-center justify-center rounded-full bg-bg-weak-50 text-center text-text-sub-600', 16 | }, 17 | variants: { 18 | size: { 19 | '80': { 20 | root: '-space-x-4', 21 | overflow: 'size-20 text-title-h5', 22 | }, 23 | '72': { 24 | root: '-space-x-4', 25 | overflow: 'size-[72px] text-title-h5', 26 | }, 27 | '64': { 28 | root: '-space-x-4', 29 | overflow: 'size-16 text-title-h5', 30 | }, 31 | '56': { 32 | root: '-space-x-4', 33 | overflow: 'size-14 text-title-h5', 34 | }, 35 | '48': { 36 | root: '-space-x-3', 37 | overflow: 'size-12 text-title-h6', 38 | }, 39 | '40': { 40 | root: '-space-x-3', 41 | overflow: 'size-10 text-label-md', 42 | }, 43 | '32': { 44 | root: '-space-x-1.5', 45 | overflow: 'size-8 text-label-sm', 46 | }, 47 | '24': { 48 | root: '-space-x-1', 49 | overflow: 'size-6 text-label-xs', 50 | }, 51 | '20': { 52 | root: '-space-x-1', 53 | overflow: 'size-5 text-subheading-2xs', 54 | }, 55 | }, 56 | }, 57 | defaultVariants: { 58 | size: '80', 59 | }, 60 | }); 61 | 62 | type AvatarGroupSharedProps = VariantProps; 63 | 64 | type AvatarGroupRootProps = VariantProps & 65 | React.HTMLAttributes; 66 | 67 | function AvatarGroupRoot({ 68 | children, 69 | size, 70 | className, 71 | ...rest 72 | }: AvatarGroupRootProps) { 73 | const uniqueId = React.useId(); 74 | const { root } = avatarGroupVariants({ size }); 75 | 76 | const sharedProps: AvatarGroupSharedProps = { 77 | size, 78 | }; 79 | 80 | const extendedChildren = recursiveCloneChildren( 81 | children as React.ReactElement[], 82 | sharedProps, 83 | [AVATAR_ROOT_NAME, AVATAR_GROUP_OVERFLOW_NAME], 84 | uniqueId, 85 | ); 86 | 87 | return ( 88 |
89 | {extendedChildren} 90 |
91 | ); 92 | } 93 | AvatarGroupRoot.displayName = AVATAR_GROUP_ROOT_NAME; 94 | 95 | function AvatarGroupOverflow({ 96 | children, 97 | size, 98 | className, 99 | ...rest 100 | }: AvatarGroupSharedProps & React.HTMLAttributes) { 101 | const { overflow } = avatarGroupVariants({ size }); 102 | 103 | return ( 104 |
105 | {children} 106 |
107 | ); 108 | } 109 | AvatarGroupOverflow.displayName = AVATAR_GROUP_OVERFLOW_NAME; 110 | 111 | export { AvatarGroupRoot as Root, AvatarGroupOverflow as Overflow }; 112 | -------------------------------------------------------------------------------- /components/ui/avatar-group-compact.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI AvatarGroupCompact v0.0.0 2 | 3 | import * as React from 'react'; 4 | import { tv, type VariantProps } from '@/utils/tv'; 5 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children'; 6 | import { AVATAR_ROOT_NAME } from '@/components/ui/avatar'; 7 | 8 | const AVATAR_GROUP_COMPACT_ROOT_NAME = 'AvatarGroupCompactRoot'; 9 | const AVATAR_GROUP_COMPACT_STACK_NAME = 'AvatarGroupCompactStack'; 10 | const AVATAR_GROUP_COMPACT_OVERFLOW_NAME = 'AvatarGroupCompactOverflow'; 11 | 12 | export const avatarGroupCompactVariants = tv({ 13 | slots: { 14 | root: 'flex w-max items-center rounded-full bg-bg-white-0 p-0.5 shadow-regular-xs', 15 | stack: 'flex -space-x-0.5 *:ring-2 *:ring-stroke-white-0', 16 | overflow: 'text-text-sub-600', 17 | }, 18 | variants: { 19 | variant: { 20 | default: {}, 21 | stroke: { 22 | root: 'ring-1 ring-stroke-soft-200', 23 | }, 24 | }, 25 | size: { 26 | '40': { 27 | overflow: 'px-2.5 text-paragraph-md', 28 | }, 29 | '32': { 30 | overflow: 'px-2 text-paragraph-sm', 31 | }, 32 | '24': { 33 | overflow: 'px-1.5 text-paragraph-xs', 34 | }, 35 | }, 36 | }, 37 | defaultVariants: { 38 | size: '40', 39 | variant: 'default', 40 | }, 41 | }); 42 | 43 | type AvatarGroupCompactSharedProps = VariantProps< 44 | typeof avatarGroupCompactVariants 45 | >; 46 | 47 | type AvatarGroupCompactRootProps = VariantProps< 48 | typeof avatarGroupCompactVariants 49 | > & 50 | React.HTMLAttributes; 51 | 52 | function AvatarGroupCompactRoot({ 53 | children, 54 | size = '40', 55 | variant, 56 | className, 57 | ...rest 58 | }: AvatarGroupCompactRootProps) { 59 | const uniqueId = React.useId(); 60 | const { root } = avatarGroupCompactVariants({ size, variant }); 61 | 62 | const sharedProps: AvatarGroupCompactSharedProps = { 63 | size, 64 | }; 65 | 66 | const extendedChildren = recursiveCloneChildren( 67 | children as React.ReactElement[], 68 | sharedProps, 69 | [AVATAR_ROOT_NAME, AVATAR_GROUP_COMPACT_OVERFLOW_NAME], 70 | uniqueId, 71 | ); 72 | 73 | return ( 74 |
75 | {extendedChildren} 76 |
77 | ); 78 | } 79 | AvatarGroupCompactRoot.displayName = AVATAR_GROUP_COMPACT_ROOT_NAME; 80 | 81 | function AvatarGroupCompactStack({ 82 | children, 83 | className, 84 | ...rest 85 | }: React.HTMLAttributes) { 86 | const { stack } = avatarGroupCompactVariants(); 87 | 88 | return ( 89 |
90 | {children} 91 |
92 | ); 93 | } 94 | AvatarGroupCompactStack.displayName = AVATAR_GROUP_COMPACT_STACK_NAME; 95 | 96 | function AvatarGroupCompactOverflow({ 97 | children, 98 | size, 99 | className, 100 | ...rest 101 | }: AvatarGroupCompactSharedProps & React.HTMLAttributes) { 102 | const { overflow } = avatarGroupCompactVariants({ size }); 103 | 104 | return ( 105 |
106 | {children} 107 |
108 | ); 109 | } 110 | AvatarGroupCompactOverflow.displayName = AVATAR_GROUP_COMPACT_OVERFLOW_NAME; 111 | 112 | export { 113 | AvatarGroupCompactRoot as Root, 114 | AvatarGroupCompactStack as Stack, 115 | AvatarGroupCompactOverflow as Overflow, 116 | }; 117 | -------------------------------------------------------------------------------- /components/ui/segmented-control.tsx: -------------------------------------------------------------------------------- 1 | // AlignUI SegmentedControl v0.0.0 2 | 3 | 'use client'; 4 | 5 | import * as React from 'react'; 6 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 7 | import mergeRefs from 'merge-refs'; 8 | import { Slottable } from '@radix-ui/react-slot'; 9 | import { cn } from '@/utils/cn'; 10 | import { useTabObserver } from '@/hooks/use-tab-observer'; 11 | 12 | const SegmentedControlRoot = TabsPrimitive.Root; 13 | SegmentedControlRoot.displayName = 'SegmentedControlRoot'; 14 | 15 | const SegmentedControlList = React.forwardRef< 16 | React.ComponentRef, 17 | React.ComponentPropsWithoutRef & { 18 | floatingBgClassName?: string; 19 | } 20 | >(({ children, className, floatingBgClassName, ...rest }, forwardedRef) => { 21 | const [lineStyle, setLineStyle] = React.useState({ width: 0, left: 0 }); 22 | 23 | const { mounted, listRef } = useTabObserver({ 24 | onActiveTabChange: (_, activeTab) => { 25 | const { offsetWidth: width, offsetLeft: left } = activeTab; 26 | setLineStyle({ width, left }); 27 | }, 28 | }); 29 | 30 | return ( 31 | 39 | {children} 40 | 41 | {/* floating bg */} 42 |