;
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 |
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 |
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 |
57 |
58 | );
59 | });
60 | SegmentedControlList.displayName = 'SegmentedControlList';
61 |
62 | const SegmentedControlTrigger = React.forwardRef<
63 | React.ComponentRef,
64 | React.ComponentPropsWithoutRef
65 | >(({ className, ...rest }, forwardedRef) => {
66 | return (
67 |
83 | );
84 | });
85 | SegmentedControlTrigger.displayName = 'SegmentedControlTrigger';
86 |
87 | const SegmentedControlContent = React.forwardRef<
88 | React.ComponentRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ ...rest }, forwardedRef) => {
91 | return ;
92 | });
93 | SegmentedControlContent.displayName = 'SegmentedControlContent';
94 |
95 | export {
96 | SegmentedControlRoot as Root,
97 | SegmentedControlList as List,
98 | SegmentedControlTrigger as Trigger,
99 | SegmentedControlContent as Content,
100 | };
101 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Tooltip v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
7 | import { tv, type VariantProps } from '@/utils/tv';
8 |
9 | const TooltipProvider = TooltipPrimitive.Provider;
10 | const TooltipRoot = TooltipPrimitive.Root;
11 | const TooltipTrigger = TooltipPrimitive.Trigger;
12 |
13 | export const tooltipVariants = tv({
14 | slots: {
15 | content: [
16 | 'z-50 shadow-tooltip',
17 | 'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
18 | ],
19 | arrow:
20 | '-translate-y-1/2 -rotate-45 border [clip-path:polygon(0_100%,0_0,100%_100%)]',
21 | },
22 | variants: {
23 | size: {
24 | xsmall: {
25 | content: 'rounded px-1.5 py-0.5 text-paragraph-xs',
26 | arrow: 'rounded-bl-sm',
27 | },
28 | small: {
29 | content: 'rounded-md px-2.5 py-1 text-paragraph-sm',
30 | arrow: 'rounded-bl-[3px]',
31 | },
32 | medium: {
33 | content: 'rounded-xl p-3 text-label-sm',
34 | arrow: 'rounded-bl-sm',
35 | },
36 | },
37 | variant: {
38 | dark: {
39 | content: 'bg-bg-strong-950 text-text-white-0',
40 | arrow: 'border-stroke-strong-950 bg-bg-strong-950',
41 | },
42 | light: {
43 | content:
44 | 'bg-bg-white-0 text-text-strong-950 ring-1 ring-stroke-soft-200',
45 | arrow: 'border-stroke-soft-200 bg-bg-white-0',
46 | },
47 | },
48 | },
49 | compoundVariants: [
50 | {
51 | size: 'xsmall',
52 | variant: 'dark',
53 | class: {
54 | arrow: 'size-1.5',
55 | },
56 | },
57 | {
58 | size: 'xsmall',
59 | variant: 'light',
60 | class: {
61 | arrow: 'size-2',
62 | },
63 | },
64 | {
65 | size: ['small', 'medium'],
66 | variant: 'dark',
67 | class: {
68 | arrow: 'size-2',
69 | },
70 | },
71 | {
72 | size: ['small', 'medium'],
73 | variant: 'light',
74 | class: {
75 | arrow: 'size-2.5',
76 | },
77 | },
78 | ],
79 | defaultVariants: {
80 | size: 'small',
81 | variant: 'dark',
82 | },
83 | });
84 |
85 | const TooltipContent = React.forwardRef<
86 | React.ComponentRef,
87 | React.ComponentPropsWithoutRef &
88 | VariantProps
89 | >(
90 | (
91 | { size, variant, className, children, sideOffset = 4, ...rest },
92 | forwardedRef,
93 | ) => {
94 | const { content, arrow } = tooltipVariants({
95 | size,
96 | variant,
97 | });
98 |
99 | return (
100 |
101 |
107 | {children}
108 |
109 |
110 |
111 |
112 |
113 | );
114 | },
115 | );
116 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
117 |
118 | export {
119 | TooltipProvider as Provider,
120 | TooltipRoot as Root,
121 | TooltipTrigger as Trigger,
122 | TooltipContent as Content,
123 | };
124 |
--------------------------------------------------------------------------------
/components/ui/tab-menu-vertical.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TabsPrimitive from '@radix-ui/react-tabs';
5 | import { cn } from '@/utils/cn';
6 | import { PolymorphicComponentProps } from '@/utils/polymorphic';
7 |
8 | const TabMenuVerticalContent = TabsPrimitive.Content;
9 | TabMenuVerticalContent.displayName = 'TabMenuVerticalContent';
10 |
11 | type TabMenuVerticalRootProps = Omit<
12 | React.ComponentPropsWithoutRef,
13 | 'orientation'
14 | >;
15 |
16 | const TabMenuVerticalRoot = React.forwardRef<
17 | React.ComponentRef,
18 | TabMenuVerticalRootProps
19 | >(({ ...rest }, forwardedRef) => {
20 | return (
21 |
22 | );
23 | });
24 | TabMenuVerticalRoot.displayName = 'TabMenuVerticalRoot';
25 |
26 | const TabMenuVerticalList = React.forwardRef<
27 | React.ComponentRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...rest }, forwardedRef) => {
30 | return (
31 |
36 | );
37 | });
38 | TabMenuVerticalList.displayName = 'TabMenuVerticalList';
39 |
40 | const TabMenuVerticalTrigger = React.forwardRef<
41 | React.ComponentRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...rest }, forwardedRef) => {
44 | return (
45 |
62 | );
63 | });
64 | TabMenuVerticalTrigger.displayName = 'TabMenuVerticalTrigger';
65 |
66 | function TabMenuVerticalIcon({
67 | className,
68 | as,
69 | ...rest
70 | }: PolymorphicComponentProps) {
71 | const Component = as || 'div';
72 |
73 | return (
74 |
85 | );
86 | }
87 | TabMenuVerticalIcon.displayName = 'TabsVerticalIcon';
88 |
89 | function TabMenuVerticalArrowIcon({
90 | className,
91 | as,
92 | ...rest
93 | }: PolymorphicComponentProps) {
94 | const Component = as || 'div';
95 |
96 | return (
97 |
109 | );
110 | }
111 | TabMenuVerticalArrowIcon.displayName = 'TabMenuVerticalArrowIcon';
112 |
113 | export {
114 | TabMenuVerticalRoot as Root,
115 | TabMenuVerticalList as List,
116 | TabMenuVerticalTrigger as Trigger,
117 | TabMenuVerticalIcon as Icon,
118 | TabMenuVerticalArrowIcon as ArrowIcon,
119 | TabMenuVerticalContent as Content,
120 | };
121 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Table v0.0.0
2 |
3 | import * as React from 'react';
4 |
5 | import * as Divider from '@/components/ui/divider';
6 | import { cn } from '@/utils/cn';
7 |
8 | const Table = React.forwardRef<
9 | HTMLTableElement,
10 | React.TableHTMLAttributes
11 | >(({ className, ...rest }, forwardedRef) => {
12 | return (
13 |
16 | );
17 | });
18 | Table.displayName = 'Table';
19 |
20 | const TableHeader = React.forwardRef<
21 | HTMLTableSectionElement,
22 | React.HTMLAttributes
23 | >(({ ...rest }, forwardedRef) => {
24 | return ;
25 | });
26 | TableHeader.displayName = 'TableHeader';
27 |
28 | const TableHead = React.forwardRef<
29 | HTMLTableCellElement,
30 | React.ThHTMLAttributes
31 | >(({ className, ...rest }, forwardedRef) => {
32 | return (
33 |
41 | );
42 | });
43 | TableHead.displayName = 'TableHead';
44 |
45 | const TableBody = React.forwardRef<
46 | HTMLTableSectionElement,
47 | React.HTMLAttributes & {
48 | spacing?: number;
49 | }
50 | >(({ spacing = 8, ...rest }, forwardedRef) => {
51 | return (
52 | <>
53 | {/* to have space between thead and tbody */}
54 |
61 |
62 |
63 | >
64 | );
65 | });
66 | TableBody.displayName = 'TableBody';
67 |
68 | const TableRow = React.forwardRef<
69 | HTMLTableRowElement,
70 | React.HTMLAttributes
71 | >(({ className, ...rest }, forwardedRef) => {
72 | return (
73 |
74 | );
75 | });
76 | TableRow.displayName = 'TableRow';
77 |
78 | function TableRowDivider({
79 | className,
80 | dividerClassName,
81 | ...rest
82 | }: React.ComponentPropsWithoutRef & {
83 | dividerClassName?: string;
84 | }) {
85 | return (
86 |
87 |
88 |
93 |
94 |
95 | );
96 | }
97 | TableRowDivider.displayName = 'TableRowDivider';
98 |
99 | const TableCell = React.forwardRef<
100 | HTMLTableCellElement,
101 | React.TdHTMLAttributes
102 | >(({ className, ...rest }, forwardedRef) => {
103 | return (
104 |
112 | );
113 | });
114 | TableCell.displayName = 'TableCell';
115 |
116 | const TableCaption = React.forwardRef<
117 | HTMLTableCaptionElement,
118 | React.HTMLAttributes
119 | >(({ className, ...rest }, forwardedRef) => (
120 |
125 | ));
126 | TableCaption.displayName = 'TableCaption';
127 |
128 | export {
129 | Table as Root,
130 | TableHeader as Header,
131 | TableBody as Body,
132 | TableHead as Head,
133 | TableRow as Row,
134 | TableRowDivider as RowDivider,
135 | TableCell as Cell,
136 | TableCaption as Caption,
137 | };
138 |
--------------------------------------------------------------------------------
/components/ui/link-button.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI LinkButton 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 { Slot } from '@radix-ui/react-slot';
7 | import { PolymorphicComponentProps } from '@/utils/polymorphic';
8 |
9 | const LINK_BUTTON_ROOT_NAME = 'LinkButtonRoot';
10 | const LINK_BUTTON_ICON_NAME = 'LinkButtonIcon';
11 |
12 | export const linkButtonVariants = tv({
13 | slots: {
14 | root: [
15 | // base
16 | 'group inline-flex items-center justify-center whitespace-nowrap outline-none',
17 | 'transition duration-200 ease-out',
18 | 'underline decoration-transparent underline-offset-[3px]',
19 | // hover
20 | 'hover:decoration-current',
21 | // focus
22 | 'focus:outline-none focus-visible:underline',
23 | // disabled
24 | 'disabled:pointer-events-none disabled:text-text-disabled-300 disabled:no-underline',
25 | ],
26 | icon: 'shrink-0',
27 | },
28 | variants: {
29 | variant: {
30 | gray: {
31 | root: [
32 | // base
33 | 'text-text-sub-600',
34 | // focus
35 | 'focus-visible:text-text-strong-950',
36 | ],
37 | },
38 | black: {
39 | root: 'text-text-strong-950',
40 | },
41 | primary: {
42 | root: [
43 | // base
44 | 'text-primary-base',
45 | // hover
46 | 'hover:text-primary-darker',
47 | ],
48 | },
49 | error: {
50 | root: [
51 | // base
52 | 'text-error-base',
53 | // hover
54 | 'hover:text-red-700',
55 | ],
56 | },
57 | modifiable: {},
58 | },
59 | size: {
60 | medium: {
61 | root: 'h-5 gap-1 text-label-sm',
62 | icon: 'size-5',
63 | },
64 | small: {
65 | root: 'h-4 gap-1 text-label-xs',
66 | icon: 'size-4',
67 | },
68 | },
69 | underline: {
70 | true: {
71 | root: 'decoration-current',
72 | },
73 | },
74 | },
75 | defaultVariants: {
76 | variant: 'gray',
77 | size: 'medium',
78 | },
79 | });
80 |
81 | type LinkButtonSharedProps = VariantProps;
82 |
83 | type LinkButtonProps = VariantProps &
84 | React.ButtonHTMLAttributes & {
85 | asChild?: boolean;
86 | };
87 |
88 | const LinkButtonRoot = React.forwardRef(
89 | (
90 | { asChild, children, variant, size, underline, className, ...rest },
91 | forwardedRef,
92 | ) => {
93 | const uniqueId = React.useId();
94 | const Component = asChild ? Slot : 'button';
95 | const { root } = linkButtonVariants({ variant, size, underline });
96 |
97 | const sharedProps: LinkButtonSharedProps = {
98 | variant,
99 | size,
100 | };
101 |
102 | const extendedChildren = recursiveCloneChildren(
103 | children as React.ReactElement[],
104 | sharedProps,
105 | [LINK_BUTTON_ICON_NAME],
106 | uniqueId,
107 | asChild,
108 | );
109 |
110 | return (
111 |
116 | {extendedChildren}
117 |
118 | );
119 | },
120 | );
121 | LinkButtonRoot.displayName = LINK_BUTTON_ROOT_NAME;
122 |
123 | function LinkButtonIcon({
124 | className,
125 | variant,
126 | size,
127 | as,
128 | ...rest
129 | }: PolymorphicComponentProps) {
130 | const Component = as || 'div';
131 | const { icon } = linkButtonVariants({ variant, size });
132 |
133 | return ;
134 | }
135 | LinkButtonIcon.displayName = LINK_BUTTON_ICON_NAME;
136 |
137 | export { LinkButtonRoot as Root, LinkButtonIcon as Icon };
138 |
--------------------------------------------------------------------------------
/components/ui/datepicker.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Datepicker v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react';
7 | import { DayPicker } from 'react-day-picker';
8 |
9 | import { compactButtonVariants } from '@/components/ui/compact-button';
10 | import { cn } from '@/utils/cn';
11 |
12 | export type CalendarProps = React.ComponentProps;
13 |
14 | function Calendar({
15 | classNames,
16 | showOutsideDays = true,
17 | ...rest
18 | }: CalendarProps) {
19 | return (
20 | ,
89 | IconRight: () => ,
90 | }}
91 | {...rest}
92 | />
93 | );
94 | }
95 |
96 | export { Calendar };
97 |
--------------------------------------------------------------------------------
/components/ui/progress-circle.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI ProgressCircle v0.0.0
2 |
3 | import * as React from 'react';
4 | import { cn } from '@/utils/cn';
5 | import { tv, type VariantProps } from '@/utils/tv';
6 |
7 | export const progressCircleVariants = tv({
8 | slots: {
9 | text: '',
10 | },
11 | variants: {
12 | size: {
13 | '80': { text: 'text-label-sm' },
14 | '72': { text: 'text-label-sm' },
15 | '64': { text: 'text-label-sm' },
16 | '56': { text: 'text-label-xs' },
17 | '48': { text: 'text-label-xs' },
18 | },
19 | },
20 | defaultVariants: {
21 | size: '80',
22 | },
23 | });
24 |
25 | function getSizes({
26 | size,
27 | }: Pick, 'size'>) {
28 | switch (size) {
29 | case '80':
30 | return {
31 | strokeWidth: 6.4,
32 | radius: 40,
33 | };
34 | case '72':
35 | return {
36 | strokeWidth: 5.75,
37 | radius: 36,
38 | };
39 | case '64':
40 | return {
41 | strokeWidth: 5.1,
42 | radius: 32,
43 | };
44 | case '56':
45 | return {
46 | strokeWidth: 4.5,
47 | radius: 28,
48 | };
49 | case '48':
50 | return {
51 | strokeWidth: 6.7,
52 | radius: 24,
53 | };
54 | default:
55 | return {
56 | strokeWidth: 6.4,
57 | radius: 40,
58 | };
59 | }
60 | }
61 |
62 | type ProgressCircleRootProps = Omit, 'value'> &
63 | VariantProps & {
64 | value?: number;
65 | max?: number;
66 | children?: React.ReactNode;
67 | };
68 |
69 | const ProgressCircleRoot = React.forwardRef<
70 | SVGSVGElement,
71 | ProgressCircleRootProps
72 | >(
73 | (
74 | {
75 | value = 0,
76 | max = 100,
77 | size,
78 | className,
79 | children,
80 | ...rest
81 | }: ProgressCircleRootProps,
82 | forwardedRef,
83 | ) => {
84 | const { text } = progressCircleVariants({ size });
85 | const { strokeWidth, radius } = getSizes({ size });
86 | const safeValue = Math.min(max, Math.max(value, 0));
87 | const normalizedRadius = radius - strokeWidth / 2;
88 | const circumference = normalizedRadius * 2 * Math.PI;
89 | const offset = circumference - (safeValue / max) * circumference;
90 |
91 | return (
92 | <>
93 |
94 |
106 |
114 | {safeValue >= 0 && (
115 |
125 | )}
126 |
127 | {children && (
128 |
134 | {children}
135 |
136 | )}
137 |
138 | >
139 | );
140 | },
141 | );
142 | ProgressCircleRoot.displayName = 'ProgressCircleRoot';
143 |
144 | export { ProgressCircleRoot as Root };
145 |
--------------------------------------------------------------------------------
/components/ui/radio.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Radio v0.0.0
2 |
3 | import * as React from 'react';
4 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
5 | import { cn } from '@/utils/cn';
6 |
7 | const RadioGroup = RadioGroupPrimitive.Root;
8 | RadioGroup.displayName = 'RadioGroup';
9 |
10 | const RadioGroupItem = React.forwardRef<
11 | React.ComponentRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...rest }, forwardedRef) => {
14 | const filterId = React.useId();
15 |
16 | return (
17 |
25 |
35 |
51 |
52 |
62 |
63 |
64 |
73 |
74 |
80 |
81 |
82 |
86 |
91 |
97 |
98 |
99 |
100 |
101 |
102 |
110 |
125 |
126 |
127 |
128 | );
129 | });
130 | RadioGroupItem.displayName = 'RadioGroupItem';
131 |
132 | export { RadioGroup as Group, RadioGroupItem as Item };
133 |
--------------------------------------------------------------------------------
/components/ui/compact-button.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI CompactButton 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 | import { Slot } from '@radix-ui/react-slot';
8 |
9 | const COMPACT_BUTTON_ROOT_NAME = 'CompactButtonRoot';
10 | const COMPACT_BUTTON_ICON_NAME = 'CompactButtonIcon';
11 |
12 | export const compactButtonVariants = tv({
13 | slots: {
14 | root: [
15 | // base
16 | 'relative flex shrink-0 items-center justify-center outline-none',
17 | 'transition duration-200 ease-out',
18 | // disabled
19 | 'disabled:pointer-events-none disabled:border-transparent disabled:bg-transparent disabled:text-text-disabled-300 disabled:shadow-none',
20 | // focus
21 | 'focus:outline-none',
22 | ],
23 | icon: '',
24 | },
25 | variants: {
26 | variant: {
27 | stroke: {
28 | root: [
29 | // base
30 | 'border border-stroke-soft-200 bg-bg-white-0 text-text-sub-600 shadow-regular-xs',
31 | // hover
32 | 'hover:border-transparent hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none',
33 | // focus
34 | 'focus-visible:border-transparent focus-visible:bg-bg-strong-950 focus-visible:text-text-white-0 focus-visible:shadow-none',
35 | ],
36 | },
37 | ghost: {
38 | root: [
39 | // base
40 | 'bg-transparent text-text-sub-600',
41 | // hover
42 | 'hover:bg-bg-weak-50 hover:text-text-strong-950',
43 | // focus
44 | 'focus-visible:bg-bg-strong-950 focus-visible:text-text-white-0',
45 | ],
46 | },
47 | white: {
48 | root: [
49 | // base
50 | 'bg-bg-white-0 text-text-sub-600 shadow-regular-xs',
51 | // hover
52 | 'hover:bg-bg-weak-50 hover:text-text-strong-950',
53 | // focus
54 | 'focus-visible:bg-bg-strong-950 focus-visible:text-text-white-0',
55 | ],
56 | },
57 | modifiable: {},
58 | },
59 | size: {
60 | large: {
61 | root: 'size-6',
62 | icon: 'size-5',
63 | },
64 | medium: {
65 | root: 'size-5',
66 | icon: 'size-[18px]',
67 | },
68 | },
69 | fullRadius: {
70 | true: {
71 | root: 'rounded-full',
72 | },
73 | false: {
74 | root: 'rounded-md',
75 | },
76 | },
77 | },
78 | defaultVariants: {
79 | variant: 'stroke',
80 | size: 'large',
81 | fullRadius: false,
82 | },
83 | });
84 |
85 | type CompactButtonSharedProps = Omit<
86 | VariantProps,
87 | 'fullRadius'
88 | >;
89 |
90 | type CompactButtonProps = VariantProps &
91 | React.ButtonHTMLAttributes & {
92 | asChild?: boolean;
93 | };
94 |
95 | const CompactButtonRoot = React.forwardRef<
96 | HTMLButtonElement,
97 | CompactButtonProps
98 | >(
99 | (
100 | { asChild, variant, size, fullRadius, children, className, ...rest },
101 | forwardedRef,
102 | ) => {
103 | const uniqueId = React.useId();
104 | const Component = asChild ? Slot : 'button';
105 | const { root } = compactButtonVariants({ variant, size, fullRadius });
106 |
107 | const sharedProps: CompactButtonSharedProps = {
108 | variant,
109 | size,
110 | };
111 |
112 | const extendedChildren = recursiveCloneChildren(
113 | children as React.ReactElement[],
114 | sharedProps,
115 | [COMPACT_BUTTON_ICON_NAME],
116 | uniqueId,
117 | asChild,
118 | );
119 |
120 | return (
121 |
126 | {extendedChildren}
127 |
128 | );
129 | },
130 | );
131 | CompactButtonRoot.displayName = COMPACT_BUTTON_ROOT_NAME;
132 |
133 | function CompactButtonIcon({
134 | variant,
135 | size,
136 | as,
137 | className,
138 | ...rest
139 | }: PolymorphicComponentProps) {
140 | const Component = as || 'div';
141 | const { icon } = compactButtonVariants({ variant, size });
142 |
143 | return ;
144 | }
145 | CompactButtonIcon.displayName = COMPACT_BUTTON_ICON_NAME;
146 |
147 | export { CompactButtonRoot as Root, CompactButtonIcon as Icon };
148 |
--------------------------------------------------------------------------------
/components/ui/status-badge.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI StatusBadge v0.0.0
2 |
3 | import * as React from 'react';
4 | import { tv, type VariantProps } from '@/utils/tv';
5 | import type { PolymorphicComponentProps } from '@/utils/polymorphic';
6 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
7 | import { Slot } from '@radix-ui/react-slot';
8 |
9 | const STATUS_BADGE_ROOT_NAME = 'StatusBadgeRoot';
10 | const STATUS_BADGE_ICON_NAME = 'StatusBadgeIcon';
11 | const STATUS_BADGE_DOT_NAME = 'StatusBadgeDot';
12 |
13 | export const statusBadgeVariants = tv({
14 | slots: {
15 | root: [
16 | 'inline-flex h-6 items-center justify-center gap-2 whitespace-nowrap rounded-md px-2 text-label-xs',
17 | 'has-[>.dot]:gap-1.5',
18 | ],
19 | icon: '-mx-1 size-4',
20 | dot: [
21 | // base
22 | 'dot -mx-1 flex size-4 items-center justify-center',
23 | // before
24 | 'before:size-1.5 before:rounded-full before:bg-current',
25 | ],
26 | },
27 | variants: {
28 | variant: {
29 | stroke: {
30 | root: 'bg-bg-white-0 text-text-sub-600 ring-1 ring-inset ring-stroke-soft-200',
31 | },
32 | light: {},
33 | },
34 | status: {
35 | completed: {
36 | icon: 'text-success-base',
37 | dot: 'text-success-base',
38 | },
39 | pending: {
40 | icon: 'text-warning-base',
41 | dot: 'text-warning-base',
42 | },
43 | failed: {
44 | icon: 'text-error-base',
45 | dot: 'text-error-base',
46 | },
47 | disabled: {
48 | icon: 'text-faded-base',
49 | dot: 'text-faded-base',
50 | },
51 | },
52 | },
53 | compoundVariants: [
54 | {
55 | variant: 'light',
56 | status: 'completed',
57 | class: {
58 | root: 'bg-success-lighter text-success-base',
59 | },
60 | },
61 | {
62 | variant: 'light',
63 | status: 'pending',
64 | class: {
65 | root: 'bg-warning-lighter text-warning-base',
66 | },
67 | },
68 | {
69 | variant: 'light',
70 | status: 'failed',
71 | class: {
72 | root: 'bg-error-lighter text-error-base',
73 | },
74 | },
75 | {
76 | variant: 'light',
77 | status: 'disabled',
78 | class: {
79 | root: 'bg-faded-lighter text-text-sub-600',
80 | },
81 | },
82 | ],
83 | defaultVariants: {
84 | status: 'disabled',
85 | variant: 'stroke',
86 | },
87 | });
88 |
89 | type StatusBadgeSharedProps = VariantProps;
90 |
91 | type StatusBadgeRootProps = React.HTMLAttributes &
92 | VariantProps & {
93 | asChild?: boolean;
94 | };
95 |
96 | const StatusBadgeRoot = React.forwardRef(
97 | (
98 | { asChild, children, variant, status, className, ...rest },
99 | forwardedRef,
100 | ) => {
101 | const uniqueId = React.useId();
102 | const Component = asChild ? Slot : 'div';
103 | const { root } = statusBadgeVariants({ variant, status });
104 |
105 | const sharedProps: StatusBadgeSharedProps = {
106 | variant,
107 | status,
108 | };
109 |
110 | const extendedChildren = recursiveCloneChildren(
111 | children as React.ReactElement[],
112 | sharedProps,
113 | [STATUS_BADGE_ICON_NAME, STATUS_BADGE_DOT_NAME],
114 | uniqueId,
115 | asChild,
116 | );
117 |
118 | return (
119 |
124 | {extendedChildren}
125 |
126 | );
127 | },
128 | );
129 | StatusBadgeRoot.displayName = STATUS_BADGE_ROOT_NAME;
130 |
131 | function StatusBadgeIcon({
132 | variant,
133 | status,
134 | className,
135 | as,
136 | }: PolymorphicComponentProps) {
137 | const Component = as || 'div';
138 | const { icon } = statusBadgeVariants({ variant, status });
139 |
140 | return ;
141 | }
142 | StatusBadgeIcon.displayName = STATUS_BADGE_ICON_NAME;
143 |
144 | function StatusBadgeDot({
145 | variant,
146 | status,
147 | className,
148 | ...rest
149 | }: StatusBadgeSharedProps & React.HTMLAttributes) {
150 | const { dot } = statusBadgeVariants({ variant, status });
151 |
152 | return
;
153 | }
154 | StatusBadgeDot.displayName = STATUS_BADGE_DOT_NAME;
155 |
156 | export {
157 | StatusBadgeRoot as Root,
158 | StatusBadgeIcon as Icon,
159 | StatusBadgeDot as Dot,
160 | };
161 |
--------------------------------------------------------------------------------
/components/ui/notification.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Notification v0.0.0
2 |
3 | import * as React from 'react';
4 | import * as Alert from '@/components/ui/alert';
5 | import { cn } from '@/utils/cn';
6 | import * as NotificationPrimitives from '@radix-ui/react-toast';
7 | import {
8 | RiAlertFill,
9 | RiCheckboxCircleFill,
10 | RiErrorWarningFill,
11 | RiInformationFill,
12 | RiMagicFill,
13 | } from '@remixicon/react';
14 |
15 | const NotificationProvider = NotificationPrimitives.Provider;
16 | const NotificationAction = NotificationPrimitives.Action;
17 |
18 | const NotificationViewport = React.forwardRef<
19 | React.ComponentRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...rest }, forwardedRef) => (
22 |
30 | ));
31 | NotificationViewport.displayName = 'NotificationViewport';
32 |
33 | type NotificationProps = React.ComponentPropsWithoutRef<
34 | typeof NotificationPrimitives.Root
35 | > &
36 | Pick<
37 | React.ComponentPropsWithoutRef,
38 | 'status' | 'variant'
39 | > & {
40 | title?: string;
41 | description?: React.ReactNode;
42 | action?: React.ReactNode;
43 | disableDismiss?: boolean;
44 | };
45 |
46 | const Notification = React.forwardRef<
47 | React.ComponentRef,
48 | NotificationProps
49 | >(
50 | (
51 | {
52 | className,
53 | status,
54 | variant = 'filled',
55 | title,
56 | description,
57 | action,
58 | disableDismiss = false,
59 | ...rest
60 | }: NotificationProps,
61 | forwardedRef,
62 | ) => {
63 | let Icon: React.ElementType;
64 |
65 | switch (status) {
66 | case 'success':
67 | Icon = RiCheckboxCircleFill;
68 | break;
69 | case 'warning':
70 | Icon = RiAlertFill;
71 | break;
72 | case 'error':
73 | Icon = RiErrorWarningFill;
74 | break;
75 | case 'information':
76 | Icon = RiInformationFill;
77 | break;
78 | case 'feature':
79 | Icon = RiMagicFill;
80 | break;
81 | default:
82 | Icon = RiErrorWarningFill;
83 | break;
84 | }
85 |
86 | return (
87 |
101 |
102 |
103 |
104 |
105 | {title && (
106 |
107 | {title}
108 |
109 | )}
110 | {description && (
111 |
112 | {description}
113 |
114 | )}
115 |
116 | {action &&
{action}
}
117 |
118 | {!disableDismiss && (
119 |
120 |
121 |
122 | )}
123 |
124 |
125 | );
126 | },
127 | );
128 | Notification.displayName = 'Notification';
129 |
130 | export {
131 | Notification as Root,
132 | NotificationProvider as Provider,
133 | NotificationAction as Action,
134 | NotificationViewport as Viewport,
135 | type NotificationProps,
136 | };
137 |
--------------------------------------------------------------------------------
/components/ui/button-group.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI ButtonGroup 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 | import { Slot } from '@radix-ui/react-slot';
8 |
9 | const BUTTON_GROUP_ROOT_NAME = 'ButtonGroupRoot';
10 | const BUTTON_GROUP_ITEM_NAME = 'ButtonGroupItem';
11 | const BUTTON_GROUP_ICON_NAME = 'ButtonGroupIcon';
12 |
13 | export const buttonGroupVariants = tv({
14 | slots: {
15 | root: 'flex -space-x-[1.5px]',
16 | item: [
17 | // base
18 | 'group relative flex items-center justify-center whitespace-nowrap bg-bg-white-0 text-center text-text-sub-600 outline-none',
19 | 'border border-stroke-soft-200',
20 | 'transition duration-200 ease-out',
21 | // hover
22 | 'hover:bg-bg-weak-50',
23 | // focus
24 | 'focus:bg-bg-weak-50 focus:outline-none',
25 | // active
26 | 'data-[state=on]:bg-bg-weak-50',
27 | 'data-[state=on]:text-text-strong-950',
28 | // disabled
29 | 'disabled:pointer-events-none disabled:bg-bg-weak-50',
30 | 'disabled:text-text-disabled-300',
31 | ],
32 | icon: 'shrink-0',
33 | },
34 | variants: {
35 | size: {
36 | small: {
37 | item: [
38 | // base
39 | 'h-9 gap-4 px-4 text-label-sm',
40 | // radius
41 | 'first:rounded-l-lg last:rounded-r-lg',
42 | ],
43 | icon: [
44 | // base
45 | '-mx-2 size-5',
46 | ],
47 | },
48 | xsmall: {
49 | item: [
50 | // base
51 | 'h-8 gap-3.5 px-3.5 text-label-sm',
52 | // radius
53 | 'first:rounded-l-lg last:rounded-r-lg',
54 | ],
55 | icon: [
56 | // base
57 | '-mx-2 size-5',
58 | ],
59 | },
60 | xxsmall: {
61 | item: [
62 | // base
63 | 'h-6 gap-3 px-3 text-label-xs',
64 | // radius
65 | 'first:rounded-l-md last:rounded-r-md',
66 | ],
67 | icon: [
68 | // base
69 | '-mx-2 size-4',
70 | ],
71 | },
72 | },
73 | },
74 | defaultVariants: {
75 | size: 'small',
76 | },
77 | });
78 |
79 | type ButtonGroupSharedProps = VariantProps;
80 |
81 | type ButtonGroupRootProps = VariantProps &
82 | React.HTMLAttributes & {
83 | asChild?: boolean;
84 | };
85 |
86 | const ButtonGroupRoot = React.forwardRef(
87 | ({ asChild, children, className, size, ...rest }, forwardedRef) => {
88 | const uniqueId = React.useId();
89 | const Component = asChild ? Slot : 'div';
90 | const { root } = buttonGroupVariants({ size });
91 |
92 | const sharedProps: ButtonGroupSharedProps = {
93 | size,
94 | };
95 |
96 | const extendedChildren = recursiveCloneChildren(
97 | children as React.ReactElement[],
98 | sharedProps,
99 | [BUTTON_GROUP_ITEM_NAME, BUTTON_GROUP_ICON_NAME],
100 | uniqueId,
101 | asChild,
102 | );
103 |
104 | return (
105 |
110 | {extendedChildren}
111 |
112 | );
113 | },
114 | );
115 | ButtonGroupRoot.displayName = BUTTON_GROUP_ROOT_NAME;
116 |
117 | type ButtonGroupItemProps = ButtonGroupSharedProps &
118 | React.ButtonHTMLAttributes & {
119 | asChild?: boolean;
120 | };
121 |
122 | const ButtonGroupItem = React.forwardRef<
123 | HTMLButtonElement,
124 | ButtonGroupItemProps
125 | >(({ children, className, size, asChild, ...rest }, forwardedRef) => {
126 | const Component = asChild ? Slot : 'button';
127 | const { item } = buttonGroupVariants({ size });
128 |
129 | return (
130 |
135 | {children}
136 |
137 | );
138 | });
139 | ButtonGroupItem.displayName = BUTTON_GROUP_ITEM_NAME;
140 |
141 | function ButtonGroupIcon({
142 | className,
143 | size,
144 | as,
145 | ...rest
146 | }: PolymorphicComponentProps) {
147 | const Component = as || 'div';
148 | const { icon } = buttonGroupVariants({ size });
149 |
150 | return ;
151 | }
152 | ButtonGroupIcon.displayName = BUTTON_GROUP_ICON_NAME;
153 |
154 | export {
155 | ButtonGroupRoot as Root,
156 | ButtonGroupItem as Item,
157 | ButtonGroupIcon as Icon,
158 | };
159 |
--------------------------------------------------------------------------------
/components/ui/fancy-button.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI FancyButton 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 { Slot } from '@radix-ui/react-slot';
7 | import { PolymorphicComponentProps } from '@/utils/polymorphic';
8 |
9 | const FANCY_BUTTON_ROOT_NAME = 'FancyButtonRoot';
10 | const FANCY_BUTTON_ICON_NAME = 'FancyButtonIcon';
11 |
12 | export const fancyButtonVariants = tv({
13 | slots: {
14 | root: [
15 | // base
16 | 'group relative inline-flex items-center justify-center whitespace-nowrap text-label-sm outline-none',
17 | 'transition duration-200 ease-out',
18 | // focus
19 | 'focus:outline-none',
20 | // disabled
21 | 'disabled:pointer-events-none disabled:text-text-disabled-300',
22 | 'disabled:bg-bg-weak-50 disabled:bg-none disabled:shadow-none disabled:before:hidden disabled:after:hidden',
23 | ],
24 | icon: 'relative z-10 size-5 shrink-0',
25 | },
26 | variants: {
27 | variant: {
28 | neutral: {
29 | root: 'bg-bg-strong-950 text-text-white-0 shadow-fancy-buttons-neutral',
30 | },
31 | primary: {
32 | root: 'bg-primary-base text-static-white shadow-fancy-buttons-primary',
33 | },
34 | destructive: {
35 | root: 'bg-error-base text-static-white shadow-fancy-buttons-error',
36 | },
37 | basic: {
38 | root: [
39 | // base
40 | 'bg-bg-white-0 text-text-sub-600 shadow-fancy-buttons-stroke',
41 | // hover
42 | 'hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none',
43 | ],
44 | },
45 | },
46 | size: {
47 | medium: {
48 | root: 'h-10 gap-3 rounded-10 px-3.5',
49 | icon: '-mx-1',
50 | },
51 | small: {
52 | root: 'h-9 gap-3 rounded-lg px-3',
53 | icon: '-mx-1',
54 | },
55 | xsmall: {
56 | root: 'h-8 gap-3 rounded-lg px-2.5',
57 | icon: '-mx-1',
58 | },
59 | },
60 | },
61 | compoundVariants: [
62 | {
63 | variant: ['neutral', 'primary', 'destructive'],
64 | class: {
65 | root: [
66 | // before
67 | 'before:pointer-events-none before:absolute before:inset-0 before:z-10 before:rounded-[inherit]',
68 | 'before:bg-gradient-to-b before:p-px',
69 | 'before:from-static-white/[.12] before:to-transparent',
70 | // before mask
71 | 'before:[mask-clip:content-box,border-box] before:[mask-composite:exclude] before:[mask-image:linear-gradient(#fff_0_0),linear-gradient(#fff_0_0)]',
72 | // after
73 | 'after:absolute after:inset-0 after:rounded-[inherit] after:bg-gradient-to-b after:from-static-white after:to-transparent',
74 | 'after:pointer-events-none after:opacity-[.16] after:transition after:duration-200 after:ease-out',
75 | // hover
76 | 'hover:after:opacity-[.24]',
77 | ],
78 | },
79 | },
80 | ],
81 | defaultVariants: {
82 | variant: 'neutral',
83 | size: 'medium',
84 | },
85 | });
86 |
87 | type FancyButtonSharedProps = VariantProps;
88 |
89 | type FancyButtonProps = VariantProps &
90 | React.ButtonHTMLAttributes & {
91 | asChild?: boolean;
92 | };
93 |
94 | const FancyButtonRoot = React.forwardRef(
95 | ({ asChild, children, variant, size, className, ...rest }, forwardedRef) => {
96 | const uniqueId = React.useId();
97 | const Component = asChild ? Slot : 'button';
98 | const { root } = fancyButtonVariants({ variant, size });
99 |
100 | const sharedProps: FancyButtonSharedProps = {
101 | variant,
102 | size,
103 | };
104 |
105 | const extendedChildren = recursiveCloneChildren(
106 | children as React.ReactElement[],
107 | sharedProps,
108 | [FANCY_BUTTON_ICON_NAME],
109 | uniqueId,
110 | asChild,
111 | );
112 |
113 | return (
114 |
119 | {extendedChildren}
120 |
121 | );
122 | },
123 | );
124 | FancyButtonRoot.displayName = FANCY_BUTTON_ROOT_NAME;
125 |
126 | function FancyButtonIcon({
127 | className,
128 | variant,
129 | size,
130 | as,
131 | ...rest
132 | }: PolymorphicComponentProps) {
133 | const Component = as || 'div';
134 | const { icon } = fancyButtonVariants({ variant, size });
135 |
136 | return ;
137 | }
138 | FancyButtonIcon.displayName = FANCY_BUTTON_ICON_NAME;
139 |
140 | export { FancyButtonRoot as Root, FancyButtonIcon as Icon };
141 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Drawer v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import * as DialogPrimitive from '@radix-ui/react-dialog';
7 | import { RiCloseLine } from '@remixicon/react';
8 |
9 | import * as CompactButton from '@/components/ui/compact-button';
10 | import { cn } from '@/utils/cn';
11 |
12 | const DrawerRoot = DialogPrimitive.Root;
13 | DrawerRoot.displayName = 'Drawer';
14 |
15 | const DrawerTrigger = DialogPrimitive.Trigger;
16 | DrawerTrigger.displayName = 'DrawerTrigger';
17 |
18 | const DrawerClose = DialogPrimitive.Close;
19 | DrawerClose.displayName = 'DrawerClose';
20 |
21 | const DrawerPortal = DialogPrimitive.Portal;
22 | DrawerPortal.displayName = 'DrawerPortal';
23 |
24 | const DrawerOverlay = React.forwardRef<
25 | React.ComponentRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, ...rest }, forwardedRef) => {
28 | return (
29 |
40 | );
41 | });
42 | DrawerOverlay.displayName = 'DrawerOverlay';
43 |
44 | const DrawerContent = React.forwardRef<
45 | React.ComponentRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, children, ...rest }, forwardedRef) => {
48 | return (
49 |
50 |
51 |
66 | {children}
67 |
68 |
69 |
70 | );
71 | });
72 | DrawerContent.displayName = 'DrawerContent';
73 |
74 | function DrawerHeader({
75 | className,
76 | children,
77 | showCloseButton = true,
78 | ...rest
79 | }: React.HTMLAttributes & {
80 | showCloseButton?: boolean;
81 | }) {
82 | return (
83 |
90 | {children}
91 |
92 | {showCloseButton && (
93 |
94 |
95 |
96 |
97 |
98 | )}
99 |
100 | );
101 | }
102 | DrawerHeader.displayName = 'DrawerHeader';
103 |
104 | const DrawerTitle = React.forwardRef<
105 | React.ComponentRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ className, ...rest }, forwardedRef) => {
108 | return (
109 |
114 | );
115 | });
116 | DrawerTitle.displayName = 'DrawerTitle';
117 |
118 | function DrawerBody({
119 | className,
120 | children,
121 | ...rest
122 | }: React.HTMLAttributes) {
123 | return (
124 |
125 | {children}
126 |
127 | );
128 | }
129 | DrawerBody.displayName = 'DrawerBody';
130 |
131 | function DrawerFooter({
132 | className,
133 | ...rest
134 | }: React.HTMLAttributes) {
135 | return (
136 |
143 | );
144 | }
145 | DrawerFooter.displayName = 'DrawerFooter';
146 |
147 | export {
148 | DrawerRoot as Root,
149 | DrawerTrigger as Trigger,
150 | DrawerClose as Close,
151 | DrawerContent as Content,
152 | DrawerHeader as Header,
153 | DrawerTitle as Title,
154 | DrawerBody as Body,
155 | DrawerFooter as Footer,
156 | };
157 |
--------------------------------------------------------------------------------
/components/ui/vertical-stepper.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI VerticalStepper v0.0.0
2 |
3 | import * as React from 'react';
4 | import { tv, type VariantProps } from '@/utils/tv';
5 | import type { PolymorphicComponentProps } from '@/utils/polymorphic';
6 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
7 | import { cn } from '@/utils/cn';
8 | import { RiArrowRightSLine } from '@remixicon/react';
9 | import { Slot } from '@radix-ui/react-slot';
10 |
11 | const VERTICAL_STEPPER_ROOT_NAME = 'VerticalStepperRoot';
12 | const VERTICAL_STEPPER_ARROW_NAME = 'VerticalStepperArrow';
13 | const VERTICAL_STEPPER_ITEM_NAME = 'VerticalStepperItem';
14 | const VERTICAL_STEPPER_ITEM_INDICATOR_NAME = 'VerticalStepperItemIndicator';
15 |
16 | function VerticalStepperRoot({
17 | asChild,
18 | children,
19 | className,
20 | ...rest
21 | }: React.HTMLAttributes & {
22 | asChild?: boolean;
23 | }) {
24 | const Component = asChild ? Slot : 'div';
25 | return (
26 |
27 | {children}
28 |
29 | );
30 | }
31 | VerticalStepperRoot.displayName = VERTICAL_STEPPER_ROOT_NAME;
32 |
33 | function VerticalStepperArrow({
34 | className,
35 | as,
36 | ...rest
37 | }: PolymorphicComponentProps) {
38 | const Component = as || RiArrowRightSLine;
39 |
40 | return (
41 |
45 | );
46 | }
47 | VerticalStepperArrow.displayName = VERTICAL_STEPPER_ARROW_NAME;
48 |
49 | const verticalStepperItemVariants = tv({
50 | slots: {
51 | root: [
52 | // base
53 | 'grid w-full auto-cols-auto grid-flow-col grid-cols-[auto,minmax(0,1fr)] items-center gap-2.5 rounded-10 p-2 text-left text-paragraph-sm',
54 | ],
55 | indicator: [
56 | // base
57 | 'flex size-5 shrink-0 items-center justify-center rounded-full text-label-xs',
58 | ],
59 | },
60 | variants: {
61 | state: {
62 | completed: {
63 | root: 'bg-bg-weak-50 text-text-sub-600',
64 | indicator: 'bg-success-base text-static-white',
65 | },
66 | active: {
67 | root: 'bg-bg-white-0 text-text-strong-950 shadow-regular-xs',
68 | indicator: 'bg-primary-base text-static-white',
69 | },
70 | default: {
71 | root: 'bg-bg-weak-50 text-text-sub-600',
72 | indicator: 'bg-bg-white-0 text-text-sub-600 shadow-regular-xs',
73 | },
74 | },
75 | },
76 | defaultVariants: {
77 | state: 'default',
78 | },
79 | });
80 |
81 | type VerticalStepperItemSharedProps = VariantProps<
82 | typeof verticalStepperItemVariants
83 | >;
84 |
85 | type VerticalStepperItemProps = React.ButtonHTMLAttributes &
86 | VariantProps & {
87 | asChild?: boolean;
88 | };
89 |
90 | const VerticalStepperItem = React.forwardRef<
91 | HTMLButtonElement,
92 | VerticalStepperItemProps
93 | >(({ asChild, children, state, className, ...rest }, forwardedRef) => {
94 | const uniqueId = React.useId();
95 | const Component = asChild ? Slot : 'button';
96 | const { root } = verticalStepperItemVariants({ state });
97 |
98 | const sharedProps: VerticalStepperItemSharedProps = {
99 | state,
100 | };
101 |
102 | const extendedChildren = recursiveCloneChildren(
103 | children as React.ReactElement[],
104 | sharedProps,
105 | [VERTICAL_STEPPER_ITEM_INDICATOR_NAME],
106 | uniqueId,
107 | asChild,
108 | );
109 |
110 | return (
111 |
116 | {extendedChildren}
117 |
118 | );
119 | });
120 | VerticalStepperItem.displayName = VERTICAL_STEPPER_ITEM_NAME;
121 |
122 | function VerticalStepperItemIndicator({
123 | state,
124 | className,
125 | children,
126 | ...rest
127 | }: React.HTMLAttributes & VerticalStepperItemSharedProps) {
128 | const { indicator } = verticalStepperItemVariants({ state });
129 |
130 | if (state === 'completed') {
131 | return (
132 |
140 | );
141 | }
142 |
143 | return (
144 |
145 | {children}
146 |
147 | );
148 | }
149 | VerticalStepperItemIndicator.displayName = VERTICAL_STEPPER_ITEM_INDICATOR_NAME;
150 |
151 | export {
152 | VerticalStepperRoot as Root,
153 | VerticalStepperArrow as Arrow,
154 | VerticalStepperItem as Item,
155 | VerticalStepperItemIndicator as ItemIndicator,
156 | };
157 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Accordion v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import * as AccordionPrimitive from '@radix-ui/react-accordion';
7 | import { cn } from '@/utils/cn';
8 | import type { PolymorphicComponentProps } from '@/utils/polymorphic';
9 | import { RiAddLine, RiSubtractLine } from '@remixicon/react';
10 |
11 | const ACCORDION_ITEM_NAME = 'AccordionItem';
12 | const ACCORDION_ICON_NAME = 'AccordionIcon';
13 | const ACCORDION_ARROW_NAME = 'AccordionArrow';
14 | const ACCORDION_TRIGGER_NAME = 'AccordionTrigger';
15 | const ACCORDION_CONTENT_NAME = 'AccordionContent';
16 |
17 | const AccordionRoot = AccordionPrimitive.Root;
18 | const AccordionHeader = AccordionPrimitive.Header;
19 |
20 | const AccordionItem = React.forwardRef<
21 | React.ComponentRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...rest }, forwardedRef) => {
24 | return (
25 |
42 | );
43 | });
44 | AccordionItem.displayName = ACCORDION_ITEM_NAME;
45 |
46 | const AccordionTrigger = React.forwardRef<
47 | React.ComponentRef,
48 | React.ComponentPropsWithoutRef
49 | >(({ children, className, ...rest }, forwardedRef) => {
50 | return (
51 |
64 | {children}
65 |
66 | );
67 | });
68 | AccordionTrigger.displayName = ACCORDION_TRIGGER_NAME;
69 |
70 | function AccordionIcon({
71 | className,
72 | as,
73 | ...rest
74 | }: PolymorphicComponentProps) {
75 | const Component = as || 'div';
76 |
77 | return (
78 |
82 | );
83 | }
84 | AccordionIcon.displayName = ACCORDION_ICON_NAME;
85 |
86 | type AccordionArrowProps = React.HTMLAttributes & {
87 | openIcon?: React.ElementType;
88 | closeIcon?: React.ElementType;
89 | };
90 |
91 | // open/close
92 | function AccordionArrow({
93 | className,
94 | openIcon: OpenIcon = RiAddLine,
95 | closeIcon: CloseIcon = RiSubtractLine,
96 | ...rest
97 | }: AccordionArrowProps) {
98 | return (
99 | <>
100 |
112 |
121 | >
122 | );
123 | }
124 | AccordionArrow.displayName = ACCORDION_ARROW_NAME;
125 |
126 | const AccordionContent = React.forwardRef<
127 | React.ComponentRef,
128 | React.ComponentPropsWithoutRef
129 | >(({ children, className, ...rest }, forwardedRef) => {
130 | return (
131 |
136 |
139 | {children}
140 |
141 |
142 | );
143 | });
144 | AccordionContent.displayName = ACCORDION_CONTENT_NAME;
145 |
146 | export {
147 | AccordionRoot as Root,
148 | AccordionHeader as Header,
149 | AccordionItem as Item,
150 | AccordionTrigger as Trigger,
151 | AccordionIcon as Icon,
152 | AccordionArrow as Arrow,
153 | AccordionContent as Content,
154 | };
155 |
--------------------------------------------------------------------------------
/components/ui/horizontal-stepper.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI HorizontalStepper v0.0.0
2 |
3 | import * as React from 'react';
4 | import { tv, type VariantProps } from '@/utils/tv';
5 | import type { PolymorphicComponentProps } from '@/utils/polymorphic';
6 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
7 | import { cn } from '@/utils/cn';
8 | import { RiArrowRightSLine } from '@remixicon/react';
9 | import { Slot } from '@radix-ui/react-slot';
10 |
11 | const HORIZONTAL_STEPPER_ROOT_NAME = 'HorizontalStepperRoot';
12 | const HORIZONTAL_STEPPER_SEPARATOR_NAME = 'HorizontalStepperSeparator';
13 | const HORIZONTAL_STEPPER_ITEM_NAME = 'HorizontalStepperItem';
14 | const HORIZONTAL_STEPPER_ITEM_INDICATOR_NAME = 'HorizontalStepperItemIndicator';
15 |
16 | function HorizontalStepperRoot({
17 | asChild,
18 | children,
19 | className,
20 | ...rest
21 | }: React.HTMLAttributes & {
22 | asChild?: boolean;
23 | }) {
24 | const Component = asChild ? Slot : 'div';
25 |
26 | return (
27 |
31 | {children}
32 |
33 | );
34 | }
35 | HorizontalStepperRoot.displayName = HORIZONTAL_STEPPER_ROOT_NAME;
36 |
37 | function HorizontalStepperSeparatorIcon({
38 | className,
39 | as,
40 | ...rest
41 | }: PolymorphicComponentProps) {
42 | const Component = as || RiArrowRightSLine;
43 |
44 | return (
45 |
49 | );
50 | }
51 | HorizontalStepperSeparatorIcon.displayName = HORIZONTAL_STEPPER_SEPARATOR_NAME;
52 |
53 | const horizontalStepperItemVariants = tv({
54 | slots: {
55 | root: [
56 | // base
57 | 'flex items-center gap-2 text-paragraph-sm',
58 | ],
59 | indicator: [
60 | // base
61 | 'flex size-5 shrink-0 items-center justify-center rounded-full text-label-xs',
62 | ],
63 | },
64 | variants: {
65 | state: {
66 | completed: {
67 | root: 'text-text-strong-950',
68 | indicator: 'bg-success-base text-static-white',
69 | },
70 | active: {
71 | root: 'text-text-strong-950',
72 | indicator: 'bg-primary-base text-static-white',
73 | },
74 | default: {
75 | root: 'text-text-sub-600',
76 | indicator:
77 | 'bg-bg-white-0 text-text-sub-600 ring-1 ring-inset ring-stroke-soft-200',
78 | },
79 | },
80 | },
81 | defaultVariants: {
82 | state: 'default',
83 | },
84 | });
85 |
86 | type HorizontalStepperItemSharedProps = VariantProps<
87 | typeof horizontalStepperItemVariants
88 | >;
89 |
90 | type HorizontalStepperItemProps =
91 | React.ButtonHTMLAttributes &
92 | VariantProps & {
93 | asChild?: boolean;
94 | };
95 |
96 | const HorizontalStepperItem = React.forwardRef<
97 | HTMLButtonElement,
98 | HorizontalStepperItemProps
99 | >(({ asChild, children, state, className, ...rest }, forwardedRef) => {
100 | const uniqueId = React.useId();
101 | const Component = asChild ? Slot : 'button';
102 | const { root } = horizontalStepperItemVariants({ state });
103 |
104 | const sharedProps: HorizontalStepperItemSharedProps = {
105 | state,
106 | };
107 |
108 | const extendedChildren = recursiveCloneChildren(
109 | children as React.ReactElement[],
110 | sharedProps,
111 | [HORIZONTAL_STEPPER_ITEM_INDICATOR_NAME],
112 | uniqueId,
113 | asChild,
114 | );
115 |
116 | return (
117 |
122 | {extendedChildren}
123 |
124 | );
125 | });
126 | HorizontalStepperItem.displayName = HORIZONTAL_STEPPER_ITEM_NAME;
127 |
128 | function HorizontalStepperItemIndicator({
129 | state,
130 | className,
131 | children,
132 | ...rest
133 | }: React.HTMLAttributes & HorizontalStepperItemSharedProps) {
134 | const { indicator } = horizontalStepperItemVariants({ state });
135 |
136 | if (state === 'completed') {
137 | return (
138 |
146 | );
147 | }
148 |
149 | return (
150 |
151 | {children}
152 |
153 | );
154 | }
155 | HorizontalStepperItemIndicator.displayName =
156 | HORIZONTAL_STEPPER_ITEM_INDICATOR_NAME;
157 |
158 | export {
159 | HorizontalStepperRoot as Root,
160 | HorizontalStepperSeparatorIcon as SeparatorIcon,
161 | HorizontalStepperItem as Item,
162 | HorizontalStepperItemIndicator as ItemIndicator,
163 | };
164 |
--------------------------------------------------------------------------------
/components/ui/svg-rating-icons.tsx:
--------------------------------------------------------------------------------
1 | /** ======== STAR ======== */
2 | export function SVGStarFill(props: React.SVGProps) {
3 | return (
4 |
10 |
14 |
15 | );
16 | }
17 |
18 | export function SVGStarHalf(props: React.SVGProps) {
19 | return (
20 |
26 |
30 |
31 | );
32 | }
33 |
34 | export function SVGStarLine(props: React.SVGProps) {
35 | return (
36 |
42 |
46 |
47 | );
48 | }
49 |
50 | export function StarRating({ rating }: { rating: number }) {
51 | const getStarIcon = (i: number) => {
52 | if (rating >= i + 1) {
53 | return ;
54 | } else if (rating >= i + 0.5) {
55 | return ;
56 | }
57 | return ;
58 | };
59 |
60 | return (
61 |
62 | {Array.from({ length: 5 }, (_, i) => getStarIcon(i))}
63 |
64 | );
65 | }
66 |
67 | /** ======== HEART ======== */
68 | export function SVGHeartFill(props: React.SVGProps) {
69 | return (
70 |
76 |
80 |
81 | );
82 | }
83 |
84 | export function SVGHeartHalf(props: React.SVGProps) {
85 | return (
86 |
92 |
96 |
97 | );
98 | }
99 |
100 | export function SVGHeartLine(props: React.SVGProps) {
101 | return (
102 |
108 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/components/ui/color-picker.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import {
5 | ColorArea as AriaColorArea,
6 | ColorAreaProps as AriaColorAreaProps,
7 | ColorField as AriaColorField,
8 | ColorPicker as AriaColorPicker,
9 | ColorSlider as AriaColorSlider,
10 | ColorSliderProps as AriaColorSliderProps,
11 | ColorSwatch as AriaColorSwatch,
12 | ColorSwatchPicker as AriaColorSwatchPicker,
13 | ColorSwatchPickerItem as AriaColorSwatchPickerItem,
14 | ColorSwatchPickerItemProps as AriaColorSwatchPickerItemProps,
15 | ColorSwatchPickerProps as AriaColorSwatchPickerProps,
16 | ColorSwatchProps as AriaColorSwatchProps,
17 | ColorThumb as AriaColorThumb,
18 | ColorThumbProps as AriaColorThumbProps,
19 | SliderTrack as AriaSliderTrack,
20 | SliderTrackProps as AriaSliderTrackProps,
21 | ColorPickerStateContext,
22 | composeRenderProps,
23 | parseColor,
24 | } from 'react-aria-components';
25 |
26 | import { cn } from '@/utils/cn';
27 |
28 | const ColorField = AriaColorField;
29 | const ColorPicker = AriaColorPicker;
30 |
31 | function ColorSlider({ className, ...props }: AriaColorSliderProps) {
32 | return (
33 |
35 | cn('py-1', className),
36 | )}
37 | {...props}
38 | />
39 | );
40 | }
41 |
42 | function ColorArea({ className, ...props }: AriaColorAreaProps) {
43 | return (
44 |
46 | cn('h-[232px] w-full rounded-lg', className),
47 | )}
48 | {...props}
49 | />
50 | );
51 | }
52 |
53 | function SliderTrack({ className, style, ...props }: AriaSliderTrackProps) {
54 | return (
55 |
57 | cn('h-2 w-full rounded-full', className),
58 | )}
59 | style={({ defaultStyle }) => ({
60 | ...style,
61 | background: `${defaultStyle.background},
62 | repeating-conic-gradient(
63 | #fff 0 90deg,
64 | rgba(0,0,0,.3) 0 180deg)
65 | 0% -25%/6px 6px`,
66 | })}
67 | {...props}
68 | />
69 | );
70 | }
71 |
72 | function ColorThumb({ className, ...props }: AriaColorThumbProps) {
73 | return (
74 |
76 | cn('z-50 size-3 rounded-full ring-2 ring-stroke-white-0', className),
77 | )}
78 | {...props}
79 | />
80 | );
81 | }
82 |
83 | function ColorSwatchPicker({
84 | className,
85 | ...props
86 | }: AriaColorSwatchPickerProps) {
87 | return (
88 |
90 | cn('flex w-full flex-wrap gap-1', className),
91 | )}
92 | {...props}
93 | />
94 | );
95 | }
96 |
97 | function ColorSwatchPickerItem({
98 | className,
99 | ...props
100 | }: AriaColorSwatchPickerItemProps) {
101 | return (
102 |
104 | cn(
105 | 'group/swatch-item cursor-pointer p-1 focus:outline-none',
106 | className,
107 | ),
108 | )}
109 | {...props}
110 | />
111 | );
112 | }
113 |
114 | function ColorSwatch({ className, style, ...props }: AriaColorSwatchProps) {
115 | return (
116 |
118 | cn(
119 | 'size-4 rounded-full border-stroke-white-0 group-data-[selected=true]/swatch-item:border-2 group-data-[selected=true]/swatch-item:ring-[1.5px]',
120 | className,
121 | ),
122 | )}
123 | style={({ defaultStyle }) => ({
124 | ...style,
125 | background: `${defaultStyle.background},
126 | repeating-conic-gradient(
127 | #fff 0 90deg,
128 | rgba(0,0,0,.3) 0 180deg)
129 | 0% -25%/6px 6px`,
130 | })}
131 | {...props}
132 | />
133 | );
134 | }
135 |
136 | const EyeDropperButton = React.forwardRef<
137 | HTMLButtonElement,
138 | React.HTMLAttributes
139 | >(({ ...rest }, forwardedRef) => {
140 | const state = React.useContext(ColorPickerStateContext)!;
141 |
142 | // eslint-disable-next-line
143 | // @ts-ignore
144 | if (typeof EyeDropper === 'undefined') {
145 | return null;
146 | }
147 |
148 | return (
149 | {
153 | // eslint-disable-next-line
154 | // @ts-ignore
155 | new EyeDropper()
156 | .open()
157 | .then((result: { sRGBHex: string }) =>
158 | state.setColor(parseColor(result.sRGBHex)),
159 | );
160 | }}
161 | {...rest}
162 | />
163 | );
164 | });
165 | EyeDropperButton.displayName = 'EyeDropperButton';
166 |
167 | export {
168 | ColorPicker as Root,
169 | ColorField as Field,
170 | ColorArea as Area,
171 | ColorSlider as Slider,
172 | SliderTrack,
173 | ColorThumb as Thumb,
174 | ColorSwatchPicker as SwatchPicker,
175 | ColorSwatchPickerItem as SwatchPickerItem,
176 | ColorSwatch as Swatch,
177 | EyeDropperButton,
178 | };
179 |
--------------------------------------------------------------------------------
/components/ui/avatar-empty-icons.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Avatar Empty Icons v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 |
7 | export function IconEmptyUser(props: React.SVGProps) {
8 | const clipPathId = React.useId();
9 |
10 | return (
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export function IconEmptyCompany(props: React.SVGProps) {
31 | const clipPathId = React.useId();
32 | const filterId1 = React.useId();
33 | const filterId2 = React.useId();
34 |
35 | return (
36 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
57 |
58 |
63 |
64 |
68 |
69 |
70 |
79 |
80 |
81 |
86 |
91 |
92 |
101 |
102 |
103 |
108 |
113 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/hooks/use-notification.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import type { NotificationProps } from '@/components/ui/notification';
5 |
6 | const NOTIFICATION_LIMIT = 1;
7 | const NOTIFICATION_REMOVE_DELAY = 1000000;
8 |
9 | type NotificationPropsWithId = NotificationProps & {
10 | id: string;
11 | };
12 |
13 | const actionTypes = {
14 | ADD_NOTIFICATION: 'ADD_NOTIFICATION',
15 | UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
16 | DISMISS_NOTIFICATION: 'DISMISS_NOTIFICATION',
17 | REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION',
18 | } as const;
19 |
20 | let count = 0;
21 |
22 | function genId() {
23 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
24 | return count.toString();
25 | }
26 |
27 | type ActionType = typeof actionTypes;
28 |
29 | type Action =
30 | | {
31 | type: ActionType['ADD_NOTIFICATION'];
32 | notification: NotificationPropsWithId;
33 | }
34 | | {
35 | type: ActionType['UPDATE_NOTIFICATION'];
36 | notification: Partial;
37 | }
38 | | {
39 | type: ActionType['DISMISS_NOTIFICATION'];
40 | notificationId?: NotificationPropsWithId['id'];
41 | }
42 | | {
43 | type: ActionType['REMOVE_NOTIFICATION'];
44 | notificationId?: NotificationPropsWithId['id'];
45 | };
46 |
47 | interface State {
48 | notifications: NotificationPropsWithId[];
49 | }
50 |
51 | const notificationTimeouts = new Map>();
52 |
53 | const addToRemoveQueue = (notificationId: string) => {
54 | if (notificationTimeouts.has(notificationId)) {
55 | return;
56 | }
57 |
58 | const timeout = setTimeout(() => {
59 | notificationTimeouts.delete(notificationId);
60 | dispatch({
61 | type: 'REMOVE_NOTIFICATION',
62 | notificationId: notificationId,
63 | });
64 | }, NOTIFICATION_REMOVE_DELAY);
65 |
66 | notificationTimeouts.set(notificationId, timeout);
67 | };
68 |
69 | export const reducer = (state: State, action: Action): State => {
70 | switch (action.type) {
71 | case 'ADD_NOTIFICATION':
72 | return {
73 | ...state,
74 | notifications: [action.notification, ...state.notifications].slice(
75 | 0,
76 | NOTIFICATION_LIMIT,
77 | ),
78 | };
79 |
80 | case 'UPDATE_NOTIFICATION':
81 | return {
82 | ...state,
83 | notifications: state.notifications.map((t) =>
84 | t.id === action.notification.id
85 | ? { ...t, ...action.notification }
86 | : t,
87 | ),
88 | };
89 |
90 | case 'DISMISS_NOTIFICATION': {
91 | const { notificationId } = action;
92 |
93 | if (notificationId) {
94 | addToRemoveQueue(notificationId);
95 | } else {
96 | state.notifications.forEach((notification) => {
97 | addToRemoveQueue(notification.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | notifications: state.notifications.map((t) =>
104 | t.id === notificationId || notificationId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t,
110 | ),
111 | };
112 | }
113 | case 'REMOVE_NOTIFICATION':
114 | if (action.notificationId === undefined) {
115 | return {
116 | ...state,
117 | notifications: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | notifications: state.notifications.filter(
123 | (t) => t.id !== action.notificationId,
124 | ),
125 | };
126 | }
127 | };
128 |
129 | const listeners: Array<(state: State) => void> = [];
130 |
131 | let memoryState: State = { notifications: [] };
132 |
133 | function dispatch(action: Action) {
134 | if (action.type === 'ADD_NOTIFICATION') {
135 | const notificationExists = memoryState.notifications.some(
136 | (t) => t.id === action.notification.id,
137 | );
138 | if (notificationExists) {
139 | return;
140 | }
141 | }
142 | memoryState = reducer(memoryState, action);
143 | listeners.forEach((listener) => {
144 | listener(memoryState);
145 | });
146 | }
147 |
148 | type Notification = Omit;
149 |
150 | function notification({ ...props }: Notification & { id?: string }) {
151 | const id = props?.id || genId();
152 |
153 | const update = (props: Notification) =>
154 | dispatch({
155 | type: 'UPDATE_NOTIFICATION',
156 | notification: { ...props, id },
157 | });
158 | const dismiss = () =>
159 | dispatch({ type: 'DISMISS_NOTIFICATION', notificationId: id });
160 |
161 | dispatch({
162 | type: 'ADD_NOTIFICATION',
163 | notification: {
164 | ...props,
165 | id,
166 | open: true,
167 | onOpenChange: (open: boolean) => {
168 | if (!open) dismiss();
169 | },
170 | },
171 | });
172 |
173 | return {
174 | id: id,
175 | dismiss,
176 | update,
177 | };
178 | }
179 |
180 | function useNotification() {
181 | const [state, setState] = React.useState(memoryState);
182 |
183 | React.useEffect(() => {
184 | listeners.push(setState);
185 | return () => {
186 | const index = listeners.indexOf(setState);
187 | if (index > -1) {
188 | listeners.splice(index, 1);
189 | }
190 | };
191 | }, [state]);
192 |
193 | return {
194 | ...state,
195 | notification,
196 | dismiss: (notificationId?: string) =>
197 | dispatch({ type: 'DISMISS_NOTIFICATION', notificationId }),
198 | };
199 | }
200 |
201 | export { notification, useNotification };
202 |
--------------------------------------------------------------------------------
/components/ui/tab-menu-horizontal.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI TabMenuHorizontal v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import { Slottable } from '@radix-ui/react-slot';
7 | import * as TabsPrimitive from '@radix-ui/react-tabs';
8 | import mergeRefs from 'merge-refs';
9 | import { cn } from '@/utils/cn';
10 | import type { PolymorphicComponentProps } from '@/utils/polymorphic';
11 | import { useTabObserver } from '@/hooks/use-tab-observer';
12 |
13 | const TabMenuHorizontalContent = TabsPrimitive.Content;
14 | TabMenuHorizontalContent.displayName = 'TabMenuHorizontalContent';
15 |
16 | const TabMenuHorizontalRoot = React.forwardRef<
17 | React.ComponentRef,
18 | Omit, 'orientation'>
19 | >(({ className, ...rest }, forwardedRef) => {
20 | return (
21 |
27 | );
28 | });
29 | TabMenuHorizontalRoot.displayName = 'TabMenuHorizontalRoot';
30 |
31 | const TabMenuHorizontalList = React.forwardRef<
32 | React.ComponentRef,
33 | React.ComponentPropsWithoutRef & {
34 | wrapperClassName?: string;
35 | }
36 | >(({ children, className, wrapperClassName, ...rest }, forwardedRef) => {
37 | const [lineStyle, setLineStyle] = React.useState({ width: 0, left: 0 });
38 | const listWrapperRef = React.useRef(null);
39 |
40 | const { mounted, listRef } = useTabObserver({
41 | onActiveTabChange: (_, activeTab) => {
42 | const { offsetWidth: width, offsetLeft: left } = activeTab;
43 | setLineStyle({ width, left });
44 |
45 | const listWrapper = listWrapperRef.current;
46 | if (listWrapper) {
47 | const containerWidth = listWrapper.clientWidth;
48 | const scrollPosition = left - containerWidth / 2 + width / 2;
49 |
50 | listWrapper.scrollTo({
51 | left: scrollPosition,
52 | behavior: 'smooth',
53 | });
54 | }
55 | },
56 | });
57 |
58 | return (
59 |
66 |
74 | {children}
75 |
76 | {/* Floating Bg */}
77 |
91 |
92 |
93 | );
94 | });
95 | TabMenuHorizontalList.displayName = 'TabMenuHorizontalList';
96 |
97 | const TabMenuHorizontalTrigger = React.forwardRef<
98 | React.ComponentRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...rest }, forwardedRef) => {
101 | return (
102 |
117 | );
118 | });
119 | TabMenuHorizontalTrigger.displayName = 'TabMenuHorizontalTrigger';
120 |
121 | function TabMenuHorizontalIcon({
122 | className,
123 | as,
124 | ...rest
125 | }: PolymorphicComponentProps) {
126 | const Component = as || 'div';
127 |
128 | return (
129 |
140 | );
141 | }
142 | TabMenuHorizontalIcon.displayName = 'TabsHorizontalIcon';
143 |
144 | function TabMenuHorizontalArrowIcon({
145 | className,
146 | as,
147 | ...rest
148 | }: PolymorphicComponentProps>) {
149 | const Component = as || 'div';
150 |
151 | return (
152 |
156 | );
157 | }
158 | TabMenuHorizontalArrowIcon.displayName = 'TabsHorizontalArrow';
159 |
160 | export {
161 | TabMenuHorizontalRoot as Root,
162 | TabMenuHorizontalList as List,
163 | TabMenuHorizontalTrigger as Trigger,
164 | TabMenuHorizontalIcon as Icon,
165 | TabMenuHorizontalArrowIcon as ArrowIcon,
166 | TabMenuHorizontalContent as Content,
167 | };
168 |
--------------------------------------------------------------------------------
/components/ui/tag.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Tag 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 { Slot } from '@radix-ui/react-slot';
7 | import { RiCloseFill } from '@remixicon/react';
8 | import { PolymorphicComponentProps } from '@/utils/polymorphic';
9 |
10 | const TAG_ROOT_NAME = 'TagRoot';
11 | const TAG_ICON_NAME = 'TagIcon';
12 | const TAG_DISMISS_BUTTON_NAME = 'TagDismissButton';
13 | const TAG_DISMISS_ICON_NAME = 'TagDismissIcon';
14 |
15 | export const tagVariants = tv({
16 | slots: {
17 | root: [
18 | 'group/tag inline-flex h-6 items-center gap-2 rounded-md px-2 text-label-xs text-text-sub-600',
19 | 'transition duration-200 ease-out',
20 | 'ring-1 ring-inset',
21 | ],
22 | icon: [
23 | // base
24 | '-mx-1 size-4 shrink-0 text-text-soft-400 transition duration-200 ease-out',
25 | // hover
26 | 'group-hover/tag:text-text-sub-600',
27 | ],
28 | dismissButton: [
29 | // base
30 | 'group/dismiss-button -ml-1.5 -mr-1 size-4 shrink-0',
31 | // focus
32 | 'focus:outline-none',
33 | ],
34 | dismissIcon: 'size-4 text-text-soft-400 transition duration-200 ease-out',
35 | },
36 | variants: {
37 | variant: {
38 | stroke: {
39 | root: [
40 | // base
41 | 'bg-bg-white-0 ring-stroke-soft-200',
42 | // hover
43 | 'hover:bg-bg-weak-50 hover:ring-transparent',
44 | // focus-within
45 | 'focus-within:bg-bg-weak-50 focus-within:ring-transparent',
46 | ],
47 | dismissIcon: [
48 | // hover
49 | 'group-hover/dismiss-button:text-text-sub-600',
50 | // focus
51 | 'group-focus/dismiss-button:text-text-sub-600',
52 | ],
53 | },
54 | gray: {
55 | root: [
56 | // base
57 | 'bg-bg-weak-50 ring-transparent',
58 | // hover
59 | 'hover:bg-bg-white-0 hover:ring-stroke-soft-200',
60 | ],
61 | },
62 | },
63 | disabled: {
64 | true: {
65 | root: 'pointer-events-none bg-bg-weak-50 text-text-disabled-300 ring-transparent',
66 | icon: 'text-text-disabled-300 [&:not(.remixicon)]:opacity-[.48]',
67 | dismissIcon: 'text-text-disabled-300',
68 | },
69 | },
70 | },
71 | defaultVariants: {
72 | variant: 'stroke',
73 | },
74 | });
75 |
76 | type TagSharedProps = VariantProps;
77 |
78 | type TagProps = VariantProps &
79 | React.HTMLAttributes & {
80 | asChild?: boolean;
81 | };
82 |
83 | const TagRoot = React.forwardRef(
84 | (
85 | { asChild, children, variant, disabled, className, ...rest },
86 | forwardedRef,
87 | ) => {
88 | const uniqueId = React.useId();
89 | const Component = asChild ? Slot : 'div';
90 | const { root } = tagVariants({ variant, disabled });
91 |
92 | const sharedProps: TagSharedProps = {
93 | variant,
94 | disabled,
95 | };
96 |
97 | const extendedChildren = recursiveCloneChildren(
98 | children as React.ReactElement[],
99 | sharedProps,
100 | [TAG_ICON_NAME, TAG_DISMISS_BUTTON_NAME, TAG_DISMISS_ICON_NAME],
101 | uniqueId,
102 | asChild,
103 | );
104 |
105 | return (
106 |
112 | {extendedChildren}
113 |
114 | );
115 | },
116 | );
117 | TagRoot.displayName = TAG_ROOT_NAME;
118 |
119 | function TagIcon({
120 | className,
121 | variant,
122 | disabled,
123 | as,
124 | ...rest
125 | }: PolymorphicComponentProps) {
126 | const Component = as || 'div';
127 | const { icon } = tagVariants({ variant, disabled });
128 |
129 | return ;
130 | }
131 | TagIcon.displayName = TAG_ICON_NAME;
132 |
133 | type TagDismissButtonProps = TagSharedProps &
134 | React.ButtonHTMLAttributes & {
135 | asChild?: boolean;
136 | };
137 |
138 | const TagDismissButton = React.forwardRef<
139 | HTMLButtonElement,
140 | TagDismissButtonProps
141 | >(
142 | (
143 | { asChild, children, className, variant, disabled, ...rest },
144 | forwardedRef,
145 | ) => {
146 | const Component = asChild ? Slot : 'button';
147 | const { dismissButton } = tagVariants({ variant, disabled });
148 |
149 | return (
150 |
155 | {children ?? (
156 |
161 | )}
162 |
163 | );
164 | },
165 | );
166 | TagDismissButton.displayName = TAG_DISMISS_BUTTON_NAME;
167 |
168 | function TagDismissIcon({
169 | className,
170 | variant,
171 | disabled,
172 | as,
173 | ...rest
174 | }: PolymorphicComponentProps) {
175 | const Component = as || 'div';
176 | const { dismissIcon } = tagVariants({ variant, disabled });
177 |
178 | return ;
179 | }
180 | TagDismissIcon.displayName = TAG_DISMISS_ICON_NAME;
181 |
182 | export {
183 | TagRoot as Root,
184 | TagIcon as Icon,
185 | TagDismissButton as DismissButton,
186 | TagDismissIcon as DismissIcon,
187 | };
188 |
--------------------------------------------------------------------------------
/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Pagination v0.0.0
2 |
3 | import * as React from 'react';
4 | import { tv, type VariantProps } from '@/utils/tv';
5 | import type { PolymorphicComponentProps } from '@/utils/polymorphic';
6 | import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
7 | import { cn } from '@/utils/cn';
8 | import { Slot } from '@radix-ui/react-slot';
9 |
10 | const PAGINATION_ROOT_NAME = 'PaginationRoot';
11 | const PAGINATION_ITEM_NAME = 'PaginationItem';
12 | const PAGINATION_NAV_BUTTON_NAME = 'PaginationNavButton';
13 | const PAGINATION_NAV_ICON_NAME = 'PaginationNavIcon';
14 |
15 | const paginationVariants = tv({
16 | slots: {
17 | root: 'flex flex-wrap items-center justify-center',
18 | item: 'flex items-center justify-center text-center text-label-sm text-text-sub-600 transition duration-200 ease-out',
19 | navButton:
20 | 'flex items-center justify-center text-text-sub-600 transition duration-200 ease-out',
21 | navIcon: 'size-5',
22 | },
23 | variants: {
24 | variant: {
25 | basic: {
26 | root: 'gap-2',
27 | item: [
28 | // base
29 | 'h-8 min-w-8 rounded-lg px-1.5 ring-1 ring-inset ring-stroke-soft-200',
30 | // hover
31 | 'hover:bg-bg-weak-50 hover:ring-transparent',
32 | ],
33 | navButton: [
34 | // base
35 | 'size-8 rounded-lg',
36 | // hover
37 | 'hover:bg-bg-weak-50',
38 | ],
39 | },
40 | rounded: {
41 | root: 'gap-2',
42 | item: [
43 | // base
44 | 'h-8 min-w-8 rounded-full px-1.5 ring-1 ring-inset ring-stroke-soft-200',
45 | // hover
46 | 'hover:bg-bg-weak-50 hover:ring-transparent',
47 | ],
48 | navButton: [
49 | // base
50 | 'size-8 rounded-full',
51 | // hover
52 | 'hover:bg-bg-weak-50',
53 | ],
54 | },
55 | group: {
56 | root: 'divide-x divide-stroke-soft-200 overflow-hidden rounded-lg border border-stroke-soft-200',
57 | item: [
58 | // base
59 | 'h-8 min-w-10 px-1.5',
60 | // hover
61 | 'hover:bg-bg-weak-50',
62 | ],
63 | navButton: [
64 | // base
65 | 'h-8 w-10 px-1.5',
66 | // hover
67 | 'hover:bg-bg-weak-50',
68 | ],
69 | },
70 | },
71 | },
72 | defaultVariants: {
73 | variant: 'basic',
74 | },
75 | });
76 |
77 | type PaginationSharedProps = VariantProps;
78 |
79 | type PaginationRootProps = React.HTMLAttributes &
80 | VariantProps & {
81 | asChild?: boolean;
82 | };
83 |
84 | function PaginationRoot({
85 | asChild,
86 | children,
87 | className,
88 | variant,
89 | ...rest
90 | }: PaginationRootProps) {
91 | const uniqueId = React.useId();
92 | const Component = asChild ? Slot : 'div';
93 | const { root } = paginationVariants({ variant });
94 |
95 | const sharedProps: PaginationSharedProps = {
96 | variant,
97 | };
98 |
99 | const extendedChildren = recursiveCloneChildren(
100 | children as React.ReactElement[],
101 | sharedProps,
102 | [
103 | PAGINATION_ITEM_NAME,
104 | PAGINATION_NAV_BUTTON_NAME,
105 | PAGINATION_NAV_ICON_NAME,
106 | ],
107 | uniqueId,
108 | asChild,
109 | );
110 |
111 | return (
112 |
113 | {extendedChildren}
114 |
115 | );
116 | }
117 | PaginationRoot.displayName = PAGINATION_ROOT_NAME;
118 |
119 | type PaginationItemProps = React.ButtonHTMLAttributes &
120 | PaginationSharedProps & {
121 | asChild?: boolean;
122 | current?: boolean;
123 | };
124 |
125 | const PaginationItem = React.forwardRef(
126 | (
127 | { asChild, children, className, variant, current, ...rest },
128 | forwardedRef,
129 | ) => {
130 | const Component = asChild ? Slot : 'button';
131 | const { item } = paginationVariants({ variant });
132 |
133 | return (
134 |
141 | {children}
142 |
143 | );
144 | },
145 | );
146 | PaginationItem.displayName = PAGINATION_ITEM_NAME;
147 |
148 | type PaginationNavButtonProps = React.ButtonHTMLAttributes &
149 | PaginationSharedProps & {
150 | asChild?: boolean;
151 | };
152 |
153 | const PaginationNavButton = React.forwardRef<
154 | HTMLButtonElement,
155 | PaginationNavButtonProps
156 | >(({ asChild, children, className, variant, ...rest }, forwardedRef) => {
157 | const Component = asChild ? Slot : 'button';
158 | const { navButton } = paginationVariants({ variant });
159 |
160 | return (
161 |
166 | {children}
167 |
168 | );
169 | });
170 | PaginationNavButton.displayName = PAGINATION_NAV_BUTTON_NAME;
171 |
172 | function PaginationNavIcon({
173 | variant,
174 | className,
175 | as,
176 | ...rest
177 | }: PolymorphicComponentProps) {
178 | const Component = as || 'div';
179 | const { navIcon } = paginationVariants({ variant });
180 |
181 | return ;
182 | }
183 | PaginationNavIcon.displayName = PAGINATION_NAV_ICON_NAME;
184 |
185 | export {
186 | PaginationRoot as Root,
187 | PaginationItem as Item,
188 | PaginationNavButton as NavButton,
189 | PaginationNavIcon as NavIcon,
190 | };
191 |
--------------------------------------------------------------------------------
/components/ui/command-menu.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI CommandMenu v0.0.0
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import { Command } from 'cmdk';
7 | import { cn } from '@/utils/cn';
8 | import { tv, type VariantProps } from '@/utils/tv';
9 | import { PolymorphicComponentProps } from '@/utils/polymorphic';
10 | import * as Modal from '@/components/ui/modal';
11 | import { type DialogProps } from '@radix-ui/react-dialog';
12 |
13 | const CommandDialogTitle = Modal.Title;
14 | const CommandDialogDescription = Modal.Description;
15 |
16 | const CommandDialog = ({
17 | children,
18 | className,
19 | overlayClassName,
20 | ...rest
21 | }: DialogProps & {
22 | className?: string;
23 | overlayClassName?: string;
24 | }) => {
25 | return (
26 |
27 |
35 | [cmdk-label]+*]:!border-t-0',
40 | )}
41 | >
42 | {children}
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | const CommandInput = React.forwardRef<
50 | React.ComponentRef,
51 | React.ComponentPropsWithoutRef
52 | >(({ className, ...rest }, forwardedRef) => {
53 | return (
54 |
71 | );
72 | });
73 | CommandInput.displayName = 'CommandInput';
74 |
75 | const CommandList = React.forwardRef<
76 | React.ComponentRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, ...rest }, forwardedRef) => {
79 | return (
80 | [cmdk-list-sizer]]:divide-y [&>[cmdk-list-sizer]]:divide-stroke-soft-200',
85 | '[&>[cmdk-list-sizer]]:overflow-auto',
86 | className,
87 | )}
88 | {...rest}
89 | />
90 | );
91 | });
92 | CommandList.displayName = 'CommandList';
93 |
94 | const CommandGroup = React.forwardRef<
95 | React.ComponentRef,
96 | React.ComponentPropsWithoutRef
97 | >(({ className, ...rest }, forwardedRef) => {
98 | return (
99 | [cmdk-group-heading]]:text-label-xs [&>[cmdk-group-heading]]:text-text-sub-600',
105 | '[&>[cmdk-group-heading]]:mb-2 [&>[cmdk-group-heading]]:px-3 [&>[cmdk-group-heading]]:pt-1',
106 | className,
107 | )}
108 | {...rest}
109 | />
110 | );
111 | });
112 | CommandGroup.displayName = 'CommandGroup';
113 |
114 | const commandItemVariants = tv({
115 | base: [
116 | 'flex items-center gap-3 rounded-10 bg-bg-white-0',
117 | 'cursor-pointer text-paragraph-sm text-text-strong-950',
118 | 'transition duration-200 ease-out',
119 | // hover/selected
120 | 'data-[selected=true]:bg-bg-weak-50',
121 | ],
122 | variants: {
123 | size: {
124 | small: 'px-3 py-2.5',
125 | medium: 'px-3 py-3',
126 | },
127 | },
128 | defaultVariants: {
129 | size: 'small',
130 | },
131 | });
132 |
133 | type CommandItemProps = VariantProps &
134 | React.ComponentPropsWithoutRef;
135 |
136 | const CommandItem = React.forwardRef<
137 | React.ComponentRef,
138 | CommandItemProps
139 | >(({ className, size, ...rest }, forwardedRef) => {
140 | return (
141 |
146 | );
147 | });
148 | CommandItem.displayName = 'CommandItem';
149 |
150 | function CommandItemIcon({
151 | className,
152 | as,
153 | ...rest
154 | }: PolymorphicComponentProps) {
155 | const Component = as || 'div';
156 |
157 | return (
158 |
162 | );
163 | }
164 |
165 | function CommandFooter({
166 | className,
167 | ...rest
168 | }: React.HTMLAttributes) {
169 | return (
170 |
177 | );
178 | }
179 |
180 | function CommandFooterKeyBox({
181 | className,
182 | ...rest
183 | }: React.HTMLAttributes) {
184 | return (
185 |
192 | );
193 | }
194 |
195 | export {
196 | CommandDialog as Dialog,
197 | CommandDialogTitle as DialogTitle,
198 | CommandDialogDescription as DialogDescription,
199 | CommandInput as Input,
200 | CommandList as List,
201 | CommandGroup as Group,
202 | CommandItem as Item,
203 | CommandItemIcon as ItemIcon,
204 | CommandFooter as Footer,
205 | CommandFooterKeyBox as FooterKeyBox,
206 | };
207 |
--------------------------------------------------------------------------------
/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Modal v0.0.0
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import * as CompactButton from '@/components/ui/compact-button';
6 | import { cn } from '@/utils/cn';
7 | import { type RemixiconComponentType, RiCloseLine } from '@remixicon/react';
8 |
9 | const ModalRoot = DialogPrimitive.Root;
10 | const ModalTrigger = DialogPrimitive.Trigger;
11 | const ModalClose = DialogPrimitive.Close;
12 | const ModalPortal = DialogPrimitive.Portal;
13 |
14 | const ModalOverlay = React.forwardRef<
15 | React.ComponentRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, ...rest }, forwardedRef) => {
18 | return (
19 |
30 | );
31 | });
32 | ModalOverlay.displayName = 'ModalOverlay';
33 |
34 | const ModalContent = React.forwardRef<
35 | React.ComponentRef,
36 | React.ComponentPropsWithoutRef & {
37 | overlayClassName?: string;
38 | showClose?: boolean;
39 | }
40 | >(
41 | (
42 | { className, overlayClassName, children, showClose = true, ...rest },
43 | forwardedRef,
44 | ) => {
45 | return (
46 |
47 |
48 |
64 | {children}
65 | {showClose && (
66 |
67 |
72 |
73 |
74 |
75 | )}
76 |
77 |
78 |
79 | );
80 | },
81 | );
82 | ModalContent.displayName = 'ModalContent';
83 |
84 | function ModalHeader({
85 | className,
86 | children,
87 | icon: Icon,
88 | title,
89 | description,
90 | ...rest
91 | }: React.HTMLAttributes & {
92 | icon?: RemixiconComponentType;
93 | title?: string;
94 | description?: string;
95 | }) {
96 | return (
97 |
104 | {children || (
105 | <>
106 | {Icon && (
107 |
108 |
109 |
110 | )}
111 | {(title || description) && (
112 |
113 | {title && {title} }
114 | {description && (
115 | {description}
116 | )}
117 |
118 | )}
119 | >
120 | )}
121 |
122 | );
123 | }
124 | ModalHeader.displayName = 'ModalHeader';
125 |
126 | const ModalTitle = React.forwardRef<
127 | React.ComponentRef,
128 | React.ComponentPropsWithoutRef
129 | >(({ className, ...rest }, forwardedRef) => {
130 | return (
131 |
136 | );
137 | });
138 | ModalTitle.displayName = 'ModalTitle';
139 |
140 | const ModalDescription = React.forwardRef<
141 | React.ComponentRef,
142 | React.ComponentPropsWithoutRef
143 | >(({ className, ...rest }, forwardedRef) => {
144 | return (
145 |
150 | );
151 | });
152 | ModalDescription.displayName = 'ModalDescription';
153 |
154 | function ModalBody({
155 | className,
156 | ...rest
157 | }: React.HTMLAttributes) {
158 | return
;
159 | }
160 | ModalBody.displayName = 'ModalBody';
161 |
162 | function ModalFooter({
163 | className,
164 | ...rest
165 | }: React.HTMLAttributes) {
166 | return (
167 |
174 | );
175 | }
176 |
177 | ModalFooter.displayName = 'ModalFooter';
178 |
179 | export {
180 | ModalRoot as Root,
181 | ModalTrigger as Trigger,
182 | ModalClose as Close,
183 | ModalPortal as Portal,
184 | ModalOverlay as Overlay,
185 | ModalContent as Content,
186 | ModalHeader as Header,
187 | ModalTitle as Title,
188 | ModalDescription as Description,
189 | ModalBody as Body,
190 | ModalFooter as Footer,
191 | };
192 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI Textarea v0.0.0
2 |
3 | import * as React from 'react';
4 | import { cn } from '@/utils/cn';
5 |
6 | const TEXTAREA_ROOT_NAME = 'TextareaRoot';
7 | const TEXTAREA_NAME = 'Textarea';
8 | const TEXTAREA_RESIZE_HANDLE_NAME = 'TextareaResizeHandle';
9 | const TEXTAREA_COUNTER_NAME = 'TextareaCounter';
10 |
11 | const Textarea = React.forwardRef<
12 | HTMLTextAreaElement,
13 | React.TextareaHTMLAttributes & {
14 | hasError?: boolean;
15 | simple?: boolean;
16 | }
17 | >(({ className, hasError, simple, disabled, ...rest }, forwardedRef) => {
18 | return (
19 |
68 | );
69 | });
70 | Textarea.displayName = TEXTAREA_NAME;
71 |
72 | function ResizeHandle() {
73 | return (
74 |
88 | );
89 | }
90 | ResizeHandle.displayName = TEXTAREA_RESIZE_HANDLE_NAME;
91 |
92 | type TextareaProps = React.TextareaHTMLAttributes &
93 | (
94 | | {
95 | simple: true;
96 | children?: never;
97 | containerClassName?: never;
98 | hasError?: boolean;
99 | }
100 | | {
101 | simple?: false;
102 | children?: React.ReactNode;
103 | containerClassName?: string;
104 | hasError?: boolean;
105 | }
106 | );
107 |
108 | const TextareaRoot = React.forwardRef(
109 | (
110 | { containerClassName, children, hasError, simple, ...rest },
111 | forwardedRef,
112 | ) => {
113 | if (simple) {
114 | return (
115 |
116 | );
117 | }
118 |
119 | return (
120 |
147 |
148 |
149 |
150 |
151 | {children}
152 |
153 |
154 |
155 |
156 |
157 |
158 | );
159 | },
160 | );
161 | TextareaRoot.displayName = TEXTAREA_ROOT_NAME;
162 |
163 | function CharCounter({
164 | current,
165 | max,
166 | className,
167 | }: {
168 | current?: number;
169 | max?: number;
170 | } & React.HTMLAttributes) {
171 | if (current === undefined || max === undefined) return null;
172 |
173 | const isError = current > max;
174 |
175 | return (
176 |
187 | {current}/{max}
188 |
189 | );
190 | }
191 | CharCounter.displayName = TEXTAREA_COUNTER_NAME;
192 |
193 | export { TextareaRoot as Root, CharCounter };
194 |
--------------------------------------------------------------------------------
/components/ui/social-button.tsx:
--------------------------------------------------------------------------------
1 | // AlignUI SocialButton 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 { Slot } from '@radix-ui/react-slot';
7 | import { PolymorphicComponentProps } from '@/utils/polymorphic';
8 |
9 | const SOCIAL_BUTTON_ROOT_NAME = 'SocialButtonRoot';
10 | const SOCIAL_BUTTON_ICON_NAME = 'SocialButtonIcon';
11 |
12 | export const socialButtonVariants = tv({
13 | slots: {
14 | root: [
15 | // base
16 | 'relative inline-flex h-10 items-center justify-center gap-3.5 whitespace-nowrap rounded-10 px-4 text-label-sm outline-none',
17 | 'transition duration-200 ease-out',
18 | // focus
19 | 'focus:outline-none',
20 | ],
21 | icon: 'relative z-10 -mx-1.5 size-5 shrink-0',
22 | },
23 | variants: {
24 | brand: {
25 | apple: {},
26 | twitter: {},
27 | google: {},
28 | facebook: {},
29 | linkedin: {},
30 | github: {},
31 | dropbox: {},
32 | },
33 | mode: {
34 | filled: {
35 | root: [
36 | // base
37 | 'text-static-white',
38 | // before
39 | 'before:pointer-events-none before:absolute before:inset-0 before:rounded-10 before:opacity-0 before:transition before:duration-200 before:ease-out',
40 | // hover
41 | 'hover:before:opacity-100',
42 | // focus
43 | 'focus-visible:shadow-button-important-focus',
44 | ],
45 | },
46 | stroke: {
47 | root: [
48 | // base
49 | 'bg-bg-white-0 text-text-strong-950 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200',
50 | // hover
51 | 'hover:bg-bg-weak-50 hover:shadow-none hover:ring-transparent',
52 | // focus
53 | 'focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950',
54 | ],
55 | },
56 | },
57 | },
58 | compoundVariants: [
59 | //#region mode=filled
60 | {
61 | brand: 'apple',
62 | mode: 'filled',
63 | class: {
64 | root: [
65 | // base
66 | 'bg-static-black',
67 | // before
68 | 'before:bg-white-alpha-16',
69 | ],
70 | },
71 | },
72 | {
73 | brand: 'twitter',
74 | mode: 'filled',
75 | class: {
76 | root: [
77 | // base
78 | 'bg-static-black',
79 | // before
80 | 'before:bg-white-alpha-16',
81 | ],
82 | },
83 | },
84 | {
85 | brand: 'google',
86 | mode: 'filled',
87 | class: {
88 | root: [
89 | // base
90 | 'bg-[#f14336]',
91 | // before
92 | 'before:bg-static-black/[.16]',
93 | ],
94 | },
95 | },
96 | {
97 | brand: 'facebook',
98 | mode: 'filled',
99 | class: {
100 | root: [
101 | // base
102 | 'bg-[#1977f3]',
103 | // before
104 | 'before:bg-static-black/[.16]',
105 | ],
106 | },
107 | },
108 | {
109 | brand: 'linkedin',
110 | mode: 'filled',
111 | class: {
112 | root: [
113 | // base
114 | 'bg-[#0077b5]',
115 | // before
116 | 'before:bg-static-black/[.16]',
117 | ],
118 | },
119 | },
120 | {
121 | brand: 'github',
122 | mode: 'filled',
123 | class: {
124 | root: [
125 | // base
126 | 'bg-[#24292f]',
127 | // before
128 | 'before:bg-white-alpha-16',
129 | ],
130 | },
131 | },
132 | {
133 | brand: 'dropbox',
134 | mode: 'filled',
135 | class: {
136 | root: [
137 | // base
138 | 'bg-[#3984ff]',
139 | // before
140 | 'before:bg-static-black/[.16]',
141 | ],
142 | },
143 | },
144 | //#endregion
145 |
146 | //#region mode=stroke
147 | {
148 | brand: 'apple',
149 | mode: 'stroke',
150 | class: {
151 | root: [
152 | // base
153 | 'text-social-apple',
154 | ],
155 | },
156 | },
157 | {
158 | brand: 'twitter',
159 | mode: 'stroke',
160 | class: {
161 | root: [
162 | // base
163 | 'text-social-twitter',
164 | ],
165 | },
166 | },
167 | {
168 | brand: 'github',
169 | mode: 'stroke',
170 | class: {
171 | root: [
172 | // base
173 | 'text-social-github',
174 | ],
175 | },
176 | },
177 | //#endregion
178 | ],
179 | defaultVariants: {
180 | mode: 'filled',
181 | },
182 | });
183 |
184 | type SocialButtonSharedProps = VariantProps;
185 |
186 | type SocialButtonProps = VariantProps &
187 | React.ButtonHTMLAttributes & {
188 | asChild?: boolean;
189 | };
190 |
191 | const SocialButtonRoot = React.forwardRef(
192 | ({ asChild, children, mode, brand, className, ...rest }, forwardedRef) => {
193 | const uniqueId = React.useId();
194 | const Component = asChild ? Slot : 'button';
195 | const { root } = socialButtonVariants({ brand, mode });
196 |
197 | const sharedProps: SocialButtonSharedProps = {
198 | mode,
199 | brand,
200 | };
201 |
202 | const extendedChildren = recursiveCloneChildren(
203 | children as React.ReactElement[],
204 | sharedProps,
205 | [SOCIAL_BUTTON_ICON_NAME],
206 | uniqueId,
207 | asChild,
208 | );
209 |
210 | return (
211 |
216 | {extendedChildren}
217 |
218 | );
219 | },
220 | );
221 | SocialButtonRoot.displayName = SOCIAL_BUTTON_ROOT_NAME;
222 |
223 | function SocialButtonIcon({
224 | brand,
225 | mode,
226 | className,
227 | as,
228 | ...rest
229 | }: PolymorphicComponentProps) {
230 | const Component = as || 'div';
231 | const { icon } = socialButtonVariants({ brand, mode });
232 |
233 | return ;
234 | }
235 | SocialButtonIcon.displayName = SOCIAL_BUTTON_ICON_NAME;
236 |
237 | export { SocialButtonRoot as Root, SocialButtonIcon as Icon };
238 |
--------------------------------------------------------------------------------
/public/images/logo.svg:
--------------------------------------------------------------------------------
1 |