├── .env-example ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .vscode └── settings.json ├── README.md ├── components ├── Navigation.tsx ├── ThemeSwitcher.tsx └── primitives │ ├── Accordian.tsx │ ├── AlertDialog.tsx │ ├── AspectRatio.tsx │ ├── Avatar.tsx │ ├── Button.tsx │ ├── Checkbox.tsx │ ├── Collapsible.tsx │ ├── ContextMenu.tsx │ ├── Dialog.tsx │ ├── Dropdown.tsx │ ├── Heading.tsx │ ├── HoverCard.tsx │ ├── Image.tsx │ ├── Input.tsx │ ├── Label.tsx │ ├── Layout.tsx │ ├── Navigation.tsx │ ├── Popover.tsx │ ├── Progress.tsx │ ├── RadioGroup.tsx │ ├── Select.tsx │ ├── Slider.tsx │ ├── Switch.tsx │ ├── Table.tsx │ ├── Tabs.tsx │ ├── Text.tsx │ ├── Toast.tsx │ ├── Toggle.tsx │ ├── ToggleGroup.tsx │ ├── Toolbar.tsx │ ├── Tooltip.tsx │ └── examples │ ├── Accordian.tsx │ ├── AlertDialog.tsx │ ├── AspectRatio.tsx │ ├── Avatar.tsx │ ├── Checkbox.tsx │ ├── Collapsible.tsx │ ├── ContextMenu.tsx │ ├── Dialog.tsx │ ├── Dropdown.tsx │ ├── HoverCard.tsx │ ├── Navigation.tsx │ ├── Popover.tsx │ ├── Progress.tsx │ ├── RadioGroup.tsx │ ├── Select.tsx │ ├── Slider.tsx │ ├── Switch.tsx │ ├── Table.tsx │ ├── Tabs.tsx │ ├── ThemeSwitcher.tsx │ ├── Toast.tsx │ ├── Toggle.tsx │ ├── ToggleGroup.tsx │ ├── Toolbar.tsx │ ├── Tooltip.tsx │ └── index.tsx ├── env ├── client.mjs ├── schema.mjs └── server.mjs ├── hooks └── useLocalStorage.ts ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── examples.ts │ └── trpc │ │ └── [trpc].ts ├── components.tsx └── index.tsx ├── postcss.config.js ├── prettier.config.js ├── prisma └── schema.prisma ├── public ├── favicon.ico └── static │ ├── Segment-Black.otf │ ├── Segment-Bold.otf │ ├── Segment-ExtraBold.otf │ ├── Segment-ExtraLight.otf │ ├── Segment-Light.otf │ ├── Segment-Medium.otf │ ├── Segment-Regular.otf │ ├── Segment-SemiBold.otf │ ├── Segment-Thin.otf │ ├── bg_curve.png │ ├── gradient_two.png │ ├── grid.svg │ └── theme.js ├── server ├── db │ └── client.ts └── trpc │ ├── context.ts │ ├── router │ ├── _app.ts │ └── example.ts │ └── trpc.ts ├── styles ├── globals.css ├── segment.css └── theme.css ├── tailwind.config.js ├── tsconfig.json ├── utils ├── trpc.ts └── tw.ts └── yarn.lock /.env-example: -------------------------------------------------------------------------------- 1 | # Since .env is gitignored, you can use .env-example to build a new `.env` file when you clone the repo. 2 | # Keep this file up-to-date when you add new variables to `.env`. 3 | 4 | # This file will be committed to version control, so make sure not to have any secrets in it. 5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. 6 | 7 | # When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly 8 | 9 | # Prisma 10 | DATABASE_URL=file:./db.sqlite 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env 34 | .env*.local 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.14.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .idea 4 | .vscode 5 | .expo 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "tailwindCSS.experimental.classRegex": [ 5 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 6 | "tw`([^`]*)", 7 | "tw\\.[^`]+`([^`]*)`", 8 | "tw\\(.*?\\).*?`([^`]*)" 9 | ] 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # T3 Tailwind Radix 2 | 3 | This is an app template started from [T3 Stack](https://github.com/t3-oss/create-t3-app) with [tailwindcss-radix](https://github.com/ecklf/tailwindcss-radix) and a nice [tailwind styled component helper](https://github.com/JackRKelly/t3-tailwind-radix/blob/master/utils/tw.ts) made by [Brendan](https://github.com/Brendonovich) for [spacedrive](https://github.com/spacedriveapp/spacedrive). 4 | 5 | Technologies used: 6 | 7 | - [Prisma](https://prisma.io) 8 | - [TailwindCSS](https://tailwindcss.com) 9 | - [TailwindCSS-Radix](https://github.com/ecklf/tailwindcss-radix) 10 | - [tRPC](https://trpc.io) 11 | - [Radix](https://www.radix-ui.com/) 12 | 13 | ## Motivation 14 | 15 | T3 Stack, Radix, and tailwindcss-radix all help reduce the time it takes to start a project. But there is still a lot of foundational work to do, such as: define props and implement animations for radix components, create non-radix primitive components, define a highly customizable theme via css variables, etc. This template aims to provide a stronger foundation for building apps with these technologies. 16 | -------------------------------------------------------------------------------- /components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { tw } from "../utils/tw"; 2 | import * as NavigationPrimitive from "./primitives/Navigation"; 3 | 4 | const CardTitle = tw.span`block text-sm font-bold text-primitive-type`; 5 | 6 | const CardBody = tw.span`block mt-1 text-sm text-primitive-type-faint`; 7 | 8 | const LinkList = tw.div`flex w-full flex-col space-y-2`; 9 | 10 | const LinkListContainer = tw.div`w-[16rem] p-3 lg:w-[18rem]`; 11 | 12 | const Skeleton = tw.div`h-12 w-full rounded-md bg-primitive-faint`; 13 | 14 | const SkeletonList = tw.div`col-span-4 flex w-full flex-col space-y-3 rounded-md bg-primitive p-4`; 15 | 16 | const SkeletonColumn = tw.div`col-span-2 w-full rounded-md bg-primitive p-4`; 17 | 18 | const SkeletonGrid = tw.div`grid grid-cols-6 gap-4`; 19 | 20 | const SkeletonGridWrapper = tw.div`w-[21rem] p-3 lg:w-[23rem]`; 21 | 22 | const RootWrapper = tw.div`fixed top-2 left-1/2 -translate-x-1/2 z-20 flex items-center justify-center`; 23 | 24 | export const Navigation = () => { 25 | return ( 26 | 27 | 28 | 29 | Home 30 | 31 | 32 | Components 33 | 34 | 35 | Resources 36 | 37 | 38 | 39 | 40 | Tailwind CSS 41 | 42 | A utility-first CSS framework for rapidly building custom user interfaces. 43 | 44 | 45 | 46 | Radix UI 47 | 48 | An open-source UI component library for building high-quality, accessible design 49 | systems and web apps. 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Overview 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { Half2Icon, MoonIcon, SunIcon } from "@radix-ui/react-icons"; 2 | import { ReactElement, useEffect, useState } from "react"; 3 | import { Button } from "./primitives/Button"; 4 | import * as Dropdown from "./primitives/Dropdown"; 5 | 6 | export type ThemeStyle = "light" | "dark" | "system"; 7 | 8 | interface Theme { 9 | key: ThemeStyle; 10 | label: string; 11 | icon: ReactElement; 12 | } 13 | 14 | const iconClassName = "mr-2 h-3.5 w-3.5 text-primitive-type-extra-faint"; 15 | 16 | export const themes: Theme[] = [ 17 | { 18 | key: "light", 19 | label: "Light", 20 | icon: 21 | }, 22 | { 23 | key: "dark", 24 | label: "Dark", 25 | icon: 26 | }, 27 | 28 | { 29 | key: "system", 30 | label: "System", 31 | icon: 32 | } 33 | ]; 34 | 35 | export const ThemeSwitcher = () => { 36 | const [preferredTheme, setPreferredTheme] = useState(null); 37 | 38 | useEffect(() => { 39 | try { 40 | let found = localStorage.getItem("theme") as ThemeStyle | null; 41 | setPreferredTheme(found); 42 | } catch (error) {} 43 | }, []); 44 | 45 | useEffect(() => { 46 | const prefersDarkQuery = window.matchMedia("(prefers-color-scheme: dark)"); 47 | const updateTheme = (_e: MediaQueryListEvent) => { 48 | setPreferredTheme("system"); 49 | }; 50 | prefersDarkQuery.addEventListener("change", updateTheme); 51 | 52 | return () => { 53 | prefersDarkQuery.removeEventListener("change", updateTheme); 54 | }; 55 | }, []); 56 | 57 | return ( 58 | 61 | {(() => { 62 | switch (preferredTheme) { 63 | case "light": 64 | return ; 65 | case "dark": 66 | return ; 67 | default: 68 | return ; 69 | } 70 | })()} 71 | 72 | } 73 | > 74 | {themes.map(({ key, label, icon }, i) => { 75 | return ( 76 | { 79 | (window as any).__setPreferredTheme(key); 80 | setPreferredTheme(key); 81 | }} 82 | icon={icon} 83 | label={label} 84 | /> 85 | ); 86 | })} 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /components/primitives/Accordian.tsx: -------------------------------------------------------------------------------- 1 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 2 | import { ChevronDownIcon } from "@radix-ui/react-icons"; 3 | import { PropsWithChildren, ReactNode } from "react"; 4 | import { tw } from "../../utils/tw"; 5 | 6 | const _Root = tw(AccordionPrimitive.Root)`space-y-4 w-full max-w-xl`; 7 | 8 | const _Trigger = tw( 9 | AccordionPrimitive.Trigger 10 | )`radix-state-closed:rounded-lg radix-state-open:rounded-t-lg group focus:outline-none inline-flex w-full items-center justify-between bg-primitive-faint hover:bg-primitive border radix-state-open:border-b-transparent border-primitive-edge px-4 py-2 text-left group-focus-within:border-transparent transition-button`; 11 | 12 | const _Item = tw( 13 | AccordionPrimitive.Item 14 | )`rounded-lg overflow-hidden group focus-within:ring focus-within:ring-highlight focus:outline-none transition-button`; 15 | 16 | const _Content = tw( 17 | AccordionPrimitive.Content 18 | )`rounded-b-lg radix-state-open:animate-collapsible-in radix-state-closed:animate-collapsible-out overflow-y-hidden w-full bg-primitive-faint transition-all`; 19 | 20 | const _Header = tw(AccordionPrimitive.Header)`w-full`; 21 | 22 | export const Header = tw.span`text-sm font-medium text-primitive-type`; 23 | 24 | export const Content = tw.div`rounded-b-lg text-sm text-primitive-type-faint px-4 pb-3 pt-1 border-x border-b border-primitive-edge transition-all group-focus-within:border-transparent`; 25 | 26 | interface ItemProps extends AccordionPrimitive.AccordionItemProps { 27 | header: ReactNode; 28 | content: ReactNode; 29 | } 30 | 31 | export const Item = (props: ItemProps) => { 32 | const { header, content, ...rest } = props; 33 | 34 | return ( 35 | <_Item {...rest}> 36 | <_Header> 37 | <_Trigger> 38 | {header} 39 | 40 | 41 | 42 | <_Content>{content} 43 | 44 | ); 45 | }; 46 | 47 | type RootProps = PropsWithChildren & 48 | (AccordionPrimitive.AccordionSingleProps | AccordionPrimitive.AccordionMultipleProps); 49 | 50 | export const Root = (props: RootProps) => { 51 | const { 52 | children, 53 | type, 54 | value, 55 | onValueChange, 56 | defaultValue, 57 | disabled, 58 | "aria-label": ariaLabel 59 | } = props; 60 | 61 | switch (type) { 62 | case "single": 63 | return ( 64 | <_Root {...{ type, value, onValueChange, defaultValue, disabled }} aria-label={ariaLabel}> 65 | {children} 66 | 67 | ); 68 | case "multiple": 69 | return ( 70 | <_Root {...{ type, value, onValueChange, defaultValue, disabled }} aria-label={ariaLabel}> 71 | {children} 72 | 73 | ); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /components/primitives/AlertDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 2 | import { PropsWithChildren, ReactNode } from "react"; 3 | import { tw } from "../../utils/tw"; 4 | import { buttonStyles } from "./Button"; 5 | 6 | const _Content = tw( 7 | AlertDialogPrimitive.Content 8 | )`radix-state-closed:animate-fade-out radix-state-open:animate-fade-in fixed z-50 w-[95vw] max-w-md rounded-lg p-4 md:w-full top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] bg-app border border-primitive-edge focus:outline-none focus-visible:ring focus-visible:ring-highlight`; 9 | 10 | const _Title = tw(AlertDialogPrimitive.Title)`text-base font-semibold text-primitive-type-bold`; 11 | 12 | const _Description = tw( 13 | AlertDialogPrimitive.Description 14 | )`mt-2 text-sm font-normal text-primitive-type`; 15 | 16 | const _Overlay = tw( 17 | AlertDialogPrimitive.Overlay 18 | )`radix-state-closed:animate-fade-out radix-state-open:animate-fade-in fixed inset-0 z-20 bg-black/50`; 19 | 20 | export const Action = (props: PropsWithChildren) => { 21 | const { children } = props; 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export const Cancel = (props: PropsWithChildren) => { 31 | const { children } = props; 32 | 33 | return ( 34 | {children} 35 | ); 36 | }; 37 | 38 | export const Description = (props: PropsWithChildren) => { 39 | const { children } = props; 40 | 41 | return <_Description>{children}; 42 | }; 43 | 44 | export const Title = (props: PropsWithChildren) => { 45 | const { children } = props; 46 | 47 | return <_Title>{children}; 48 | }; 49 | 50 | interface RootProps extends PropsWithChildren, AlertDialogPrimitive.AlertDialogProps { 51 | trigger: ReactNode; 52 | } 53 | 54 | export const Root = (props: RootProps) => { 55 | const { trigger, children, ...rest } = props; 56 | 57 | return ( 58 | 59 | {trigger} 60 | <_Overlay /> 61 | <_Content>{children} 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /components/primitives/AspectRatio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; 2 | import { PropsWithChildren } from "react"; 3 | 4 | interface Props extends PropsWithChildren, AspectRatioPrimitive.AspectRatioProps {} 5 | 6 | export const Root = (props: Props) => { 7 | const { children, ...rest } = props; 8 | 9 | return {children}; 10 | }; 11 | -------------------------------------------------------------------------------- /components/primitives/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 2 | import cx from "clsx"; 3 | import { tw } from "../../utils/tw"; 4 | 5 | export enum Variant { 6 | Circle, 7 | Rounded 8 | } 9 | 10 | const _Root = tw(AvatarPrimitive.Root)`relative inline-flex h-10 w-10`; 11 | 12 | const _Image = tw(AvatarPrimitive.Image)`h-full w-full object-cover`; 13 | 14 | const _OnlineBubble = tw.span`block h-2.5 w-2.5 rounded-full bg-green-400`; 15 | 16 | const _OnlineBubbleWrapper = tw.div`absolute bottom-0 right-0 h-2 w-2`; 17 | 18 | const _Initials = tw.span`text-sm font-medium uppercase text-primitive-type transition-colors`; 19 | 20 | const _Fallback = tw( 21 | AvatarPrimitive.Fallback 22 | )`flex h-full w-full items-center justify-center bg-primitive transition-colors`; 23 | 24 | interface RootProps extends AvatarPrimitive.AvatarImageProps { 25 | variant?: Variant; 26 | renderInvalidUrls?: boolean; 27 | online?: boolean; 28 | initials?: `${string}${string}`; 29 | delayMs?: number; 30 | } 31 | 32 | const Root = (props: RootProps) => { 33 | const { variant = Variant.Rounded, online, initials, src, alt, delayMs = 600 } = props; 34 | 35 | return ( 36 | <_Root> 37 | <_Image 38 | {...{ src, alt }} 39 | className={cx( 40 | { 41 | [Variant.Circle]: "rounded-full", 42 | [Variant.Rounded]: "rounded" 43 | }[variant] 44 | )} 45 | /> 46 | {online && ( 47 | <_OnlineBubbleWrapper 48 | className={cx( 49 | { 50 | [Variant.Circle]: "-translate-x-1/2 -translate-y-1/2", 51 | [Variant.Rounded]: "" 52 | }[variant] 53 | )} 54 | > 55 | <_OnlineBubble /> 56 | 57 | )} 58 | <_Fallback 59 | className={cx( 60 | { 61 | [Variant.Circle]: "rounded-full", 62 | [Variant.Rounded]: "rounded" 63 | }[variant] 64 | )} 65 | delayMs={delayMs} 66 | > 67 | <_Initials>{initials} 68 | 69 | 70 | ); 71 | }; 72 | 73 | Root.variant = Variant; 74 | export { Root }; 75 | -------------------------------------------------------------------------------- /components/primitives/Button.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from "class-variance-authority"; 2 | import clsx from "clsx"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | 6 | type LinkProps = { 7 | type: "link"; 8 | } & React.ComponentProps<"a"> & 9 | VariantProps; 10 | 11 | type ButtonProps = { 12 | type?: "button"; 13 | } & React.ComponentProps<"button"> & 14 | VariantProps; 15 | 16 | type NextLinkProps = { 17 | type: "next-link"; 18 | } & React.ComponentProps & 19 | VariantProps; 20 | 21 | type PolymorphicProps = LinkProps | ButtonProps | NextLinkProps; 22 | 23 | type PolymorphicButton = { 24 | (props: LinkProps): JSX.Element; 25 | (props: ButtonProps): JSX.Element; 26 | (props: NextLinkProps): JSX.Element; 27 | }; 28 | 29 | export const buttonStyles = cva( 30 | "inline-flex select-none items-center justify-center focus:outline-none focus-visible:ring focus-visible:ring-highlight group transition-button focus-visible:border-transparent", 31 | { 32 | variants: { 33 | rounded: { 34 | md: "rounded-md" 35 | }, 36 | size: { 37 | md: "px-4 py-2", 38 | sm: "px-2 py-2" 39 | }, 40 | shade: { 41 | none: "", 42 | primary: "bg-primary text-white hover:bg-primary-bold", 43 | primitive: 44 | "bg-primitive-faint border border-primitive-edge text-primitive-type hover:bg-primitive" 45 | }, 46 | fontSize: { 47 | md: "text-base", 48 | sm: "text-sm", 49 | xs: "text-xs" 50 | }, 51 | fontWeight: { 52 | thin: "font-thin", 53 | extralight: "font-extralight", 54 | light: "font-light", 55 | normal: "font-normal", 56 | medium: "font-medium", 57 | semibold: "font-semibold", 58 | bold: "font-bold", 59 | extrabold: "font-extrabold" 60 | } 61 | }, 62 | defaultVariants: { 63 | fontSize: "sm", 64 | fontWeight: "medium", 65 | size: "md", 66 | rounded: "md", 67 | shade: "primitive" 68 | } 69 | } 70 | ); 71 | 72 | export const Button = React.forwardRef( 73 | (props, ref) => { 74 | const { type, className, children, ...rest } = props; 75 | 76 | switch (type) { 77 | case "link": 78 | return ( 79 | } 82 | {...(rest as LinkProps)} 83 | > 84 | {children} 85 | 86 | ); 87 | case "next-link": 88 | return ( 89 | } 92 | {...(rest as NextLinkProps)} 93 | > 94 | {children} 95 | 96 | ); 97 | default: 98 | return ( 99 | 106 | ); 107 | } 108 | } 109 | ) as PolymorphicButton; 110 | -------------------------------------------------------------------------------- /components/primitives/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 2 | import { CheckIcon } from "@radix-ui/react-icons"; 3 | import clsx from "clsx"; 4 | import { ReactNode } from "react"; 5 | import { tw } from "../../utils/tw"; 6 | import { Label } from "./Label"; 7 | 8 | const _Root = tw( 9 | CheckboxPrimitive.Root 10 | )`flex h-5 w-5 items-center justify-center rounded radix-state-checked:bg-primary radix-state-unchecked:bg-primitive-faint radix-state-unchecked:border border-primitive-edge focus-visible:border-transparent focus:outline-none focus-visible:ring focus-visible:ring-highlight transition-button`; 11 | 12 | interface RootProps extends CheckboxPrimitive.CheckboxProps { 13 | id: string; 14 | label: ReactNode; 15 | } 16 | 17 | export const Root = (props: RootProps) => { 18 | const { id, label, ...rest } = props; 19 | 20 | return ( 21 | <> 22 | <_Root 23 | {...{ 24 | id, 25 | ...rest 26 | }} 27 | > 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /components/primitives/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 2 | import { CaretRightIcon } from "@radix-ui/react-icons"; 3 | import React, { PropsWithChildren, ReactNode } from "react"; 4 | import { tw } from "../../utils/tw"; 5 | 6 | const _Trigger = tw( 7 | CollapsiblePrimitive.Trigger 8 | )`group flex w-full select-none items-center justify-between rounded-md px-4 py-2 text-left text-sm font-medium bg-primitive-faint hover:bg-primitive border border-primitive-edge text-primitive-type-bold focus:outline-none focus-visible:ring focus-visible:ring-highlight transition-button focus-visible:border-transparent w-full`; 9 | 10 | const _Content = tw( 11 | CollapsiblePrimitive.Content 12 | )`radix-state-open:animate-collapsible-in radix-state-closed:animate-collapsible-out overflow-y-hidden mt-4 flex flex-col space-y-4`; 13 | 14 | const _Item = tw.div`group ml-12 flex select-none items-center justify-between rounded-md px-4 py-2 text-left text-sm font-medium bg-primitive-faint text-primitive-type-bold hover:bg-primitive border border-primitive-edge transition-colors`; 15 | 16 | const _Root = tw(CollapsiblePrimitive.Root)`w-full max-w-xl`; 17 | 18 | interface ItemProps extends PropsWithChildren {} 19 | 20 | export const Item = (props: ItemProps) => { 21 | const { children } = props; 22 | 23 | return <_Item>{children}; 24 | }; 25 | 26 | interface RootProps extends PropsWithChildren { 27 | trigger: ReactNode; 28 | } 29 | 30 | export const Root = (props: RootProps) => { 31 | const { children, trigger } = props; 32 | 33 | const [isOpen, setIsOpen] = React.useState(true); 34 | 35 | return ( 36 | <_Root open={isOpen} onOpenChange={setIsOpen}> 37 | <_Trigger> 38 |
{trigger}
39 | 40 | 41 | <_Content>{children} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /components/primitives/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; 2 | import { CaretRightIcon, CheckIcon } from "@radix-ui/react-icons"; 3 | import { PropsWithChildren, ReactNode } from "react"; 4 | import { tw } from "../../utils/tw"; 5 | 6 | const _Content = tw( 7 | ContextMenuPrimitive.Content 8 | )`animate-all-sides w-48 md:w-56 bg-primitive-faint backdrop-blur bg-opacity-[90%] z-10 rounded-lg`; 9 | 10 | const _ContentInner = tw.div`border border-primitive-edge rounded-lg px-1.5 py-1`; 11 | 12 | const _SubContent = tw( 13 | ContextMenuPrimitive.SubContent 14 | )`origin-radix-dropdown-menu animate-open-all-sides animate-close-all-sides w-full rounded-md px-1 py-1 text-xs border border-primitive-edge bg-primitive-faint backdrop-blur bg-opacity-[90%] z-[11]`; 15 | 16 | const _SubTrigger = tw( 17 | ContextMenuPrimitive.SubTrigger 18 | )`flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-none text-primitive-type-faint focus:bg-primitive`; 19 | 20 | const _ItemLabelGrow = tw.span`flex-grow text-primitive-type`; 21 | 22 | const _ShortcutLabel = tw.span`text-xs ml-2 text-primitive-type-extra-faint`; 23 | 24 | const _Arrow = tw(ContextMenuPrimitive.Arrow)`fill-current text-primitive-edge`; 25 | 26 | const _Trigger = tw( 27 | ContextMenuPrimitive.Trigger 28 | )`inline-flex w-36 items-center justify-center rounded-md border-2 border-dashed border-primitive-edge bg-primitive-faint px-3 py-4 transition-colors`; 29 | 30 | const _TriggerInner = tw.span`select-none text-sm font-medium text-primitive-type`; 31 | 32 | const _Item = tw( 33 | ContextMenuPrimitive.Item 34 | )`flex cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-none text-primitive-type-faint focus:bg-primitive min-w-[8rem]`; 35 | 36 | const _CheckboxItem = tw( 37 | ContextMenuPrimitive.CheckboxItem 38 | )`flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-none text-primitive-type-faint focus:bg-primitive`; 39 | 40 | const _Label = tw( 41 | ContextMenuPrimitive.Label 42 | )`select-none px-2 py-2 text-xs text-primitive-type-extra-faint`; 43 | 44 | export const Separator = tw(ContextMenuPrimitive.Separator)`my-1 h-px bg-primitive-bold`; 45 | 46 | export const Trigger = (props: PropsWithChildren) => { 47 | const { children } = props; 48 | 49 | return <_TriggerInner>{children}; 50 | }; 51 | 52 | interface LabelProps { 53 | label: ReactNode; 54 | } 55 | 56 | export const Label = (props: LabelProps) => { 57 | const { label } = props; 58 | 59 | return <_Label>{label}; 60 | }; 61 | 62 | interface SubProps 63 | extends PropsWithChildren, 64 | ContextMenuPrimitive.ContextMenuSubProps, 65 | ContextMenuPrimitive.ContextMenuSubContentProps { 66 | icon?: React.ReactNode; 67 | label: React.ReactNode; 68 | className?: string; 69 | } 70 | 71 | export const Sub = (props: SubProps) => { 72 | const { label, icon, children, defaultOpen, onOpenChange, open, sideOffset = 6, ...rest } = props; 73 | 74 | return ( 75 | 76 | <_SubTrigger> 77 | {icon} 78 | <_ItemLabelGrow>{label} 79 | 80 | 81 | 82 | <_SubContent {...{ sideOffset, ...rest }}>{children} 83 | 84 | 85 | ); 86 | }; 87 | 88 | interface CheckboxItemProps 89 | extends PropsWithChildren, 90 | ContextMenuPrimitive.ContextMenuCheckboxItemProps { 91 | icon?: React.ReactNode; 92 | checkedIcon?: React.ReactNode; 93 | label: React.ReactNode; 94 | } 95 | 96 | export const CheckboxItem = (props: CheckboxItemProps) => { 97 | const { label, icon, checked, checkedIcon, ...rest } = props; 98 | 99 | return ( 100 | <_CheckboxItem {...{ checked, ...rest }}> 101 | {(() => { 102 | if (checked) { 103 | return checkedIcon; 104 | } else { 105 | return icon; 106 | } 107 | })()} 108 | <_ItemLabelGrow>{label} 109 | 110 | 111 | 112 | 113 | ); 114 | }; 115 | 116 | interface ItemProps { 117 | icon: React.ReactNode; 118 | shortcut?: string; 119 | label: React.ReactNode; 120 | } 121 | 122 | export const Item = (props: ItemProps) => { 123 | const { label, icon, shortcut } = props; 124 | 125 | return ( 126 | <_Item> 127 | {icon} 128 | <_ItemLabelGrow>{label} 129 | {shortcut && <_ShortcutLabel>{shortcut}} 130 | 131 | ); 132 | }; 133 | 134 | interface RootProps extends PropsWithChildren, ContextMenuPrimitive.ContextMenuProps { 135 | trigger: React.ReactNode; 136 | className?: string; 137 | } 138 | 139 | export const Root = (props: RootProps) => { 140 | const { children, trigger, ...rest } = props; 141 | 142 | return ( 143 | 144 | <_Trigger asChild> 145 | <_TriggerInner>{trigger} 146 | 147 | 148 | <_Content> 149 | <_ContentInner> 150 | <_Arrow /> 151 | {children} 152 | 153 | 154 | 155 | 156 | ); 157 | }; 158 | -------------------------------------------------------------------------------- /components/primitives/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 2 | import { Cross1Icon } from "@radix-ui/react-icons"; 3 | import { PropsWithChildren, ReactNode } from "react"; 4 | import { tw } from "../../utils/tw"; 5 | import { buttonStyles } from "./Button"; 6 | 7 | const _Close = tw( 8 | DialogPrimitive.Close 9 | )`absolute top-3.5 right-3.5 inline-flex items-center justify-center rounded-full p-1 focus:outline-none focus-visible:ring focus-visible:ring-highlight transition-button`; 10 | 11 | const _Overlay = tw( 12 | DialogPrimitive.Overlay 13 | )`radix-state-closed:animate-fade-out radix-state-open:animate-fade-in fixed inset-0 z-20 bg-black/50`; 14 | 15 | const _Content = tw( 16 | DialogPrimitive.Content 17 | )`radix-state-closed:animate-fade-out radix-state-open:animate-fade-in fixed z-50 w-[95vw] max-w-md rounded-lg p-4 md:w-full top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] bg-app border border-primitive-edge focus:outline-none focus-visible:ring focus-visible:ring-highlight`; 18 | 19 | const _Title = tw(DialogPrimitive.Title)`text-base font-semibold text-primitive-type-bold`; 20 | 21 | const _Description = tw(DialogPrimitive.Description)`mt-2 text-sm font-normal text-primitive-type`; 22 | 23 | export const Action = (props: PropsWithChildren) => { 24 | const { children } = props; 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | export const Cancel = (props: PropsWithChildren) => { 34 | const { children } = props; 35 | 36 | return {children}; 37 | }; 38 | 39 | export const Description = (props: PropsWithChildren) => { 40 | const { children } = props; 41 | 42 | return <_Description>{children}; 43 | }; 44 | 45 | export const Title = (props: PropsWithChildren) => { 46 | const { children } = props; 47 | 48 | return <_Title>{children}; 49 | }; 50 | 51 | interface RootProps 52 | extends PropsWithChildren, 53 | Pick { 54 | trigger: ReactNode; 55 | showCloseIcon?: boolean; 56 | } 57 | 58 | export const Root = (props: RootProps) => { 59 | const { onOpenChange, open, trigger, children, showCloseIcon = true } = props; 60 | 61 | return ( 62 | 63 | {trigger} 64 | <_Overlay /> 65 | <_Content> 66 | {children} 67 | {showCloseIcon && ( 68 | <_Close> 69 | 70 | 71 | )} 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /components/primitives/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 2 | import { CaretRightIcon, CheckIcon } from "@radix-ui/react-icons"; 3 | import { PropsWithChildren, ReactElement, ReactNode } from "react"; 4 | import { tw } from "../../utils/tw"; 5 | 6 | const _Content = tw( 7 | DropdownMenuPrimitive.Content 8 | )`animate-all-sides w-48 md:w-56 bg-primitive-faint backdrop-blur bg-opacity-[90%] z-10 rounded-lg`; 9 | 10 | const _ContentInner = tw.div`border border-primitive-edge rounded-lg px-1.5 py-1`; 11 | 12 | const _Item = tw( 13 | DropdownMenuPrimitive.Item 14 | )`flex cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-none text-primitive-type-faint focus:bg-primitive min-w-[8rem]`; 15 | 16 | const _CheckboxItem = tw( 17 | DropdownMenuPrimitive.CheckboxItem 18 | )`flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-none text-primitive-type-faint focus:bg-primitive`; 19 | 20 | const _Label = tw( 21 | DropdownMenuPrimitive.Label 22 | )`select-none px-2 py-2 text-xs text-primitive-type-extra-faint`; 23 | 24 | const _SubContent = tw( 25 | DropdownMenuPrimitive.SubContent 26 | )`origin-radix-dropdown-menu radix-side-right:animate-scale-in w-full rounded-md px-1 py-1 text-xs border border-primitive-edge bg-primitive-faint backdrop-blur bg-opacity-[90%] z-[11]`; 27 | 28 | const _SubTrigger = tw( 29 | DropdownMenuPrimitive.SubTrigger 30 | )`flex w-full cursor-default select-none items-center rounded-md px-2 py-2 text-xs outline-none text-primitive-type-faint focus:bg-primitive`; 31 | 32 | const _ItemLabelGrow = tw.span`flex-grow text-primitive-type`; 33 | 34 | const _ShortcutLabel = tw.span`text-xs ml-2 text-primitive-type-extra-faint`; 35 | 36 | const _Arrow = tw(DropdownMenuPrimitive.Arrow)`fill-current text-primitive-edge`; 37 | 38 | export const Separator = tw(DropdownMenuPrimitive.Separator)`my-1 h-px bg-primitive-bold`; 39 | 40 | interface LabelProps { 41 | label: ReactNode; 42 | } 43 | 44 | export const Label = (props: LabelProps) => { 45 | const { label } = props; 46 | 47 | return <_Label>{label}; 48 | }; 49 | 50 | interface SubProps 51 | extends PropsWithChildren, 52 | DropdownMenuPrimitive.DropdownMenuSubProps, 53 | DropdownMenuPrimitive.DropdownMenuSubContentProps { 54 | icon?: ReactElement; 55 | label: ReactNode; 56 | className?: string; 57 | } 58 | 59 | export const Sub = (props: SubProps) => { 60 | const { label, icon, children, defaultOpen, onOpenChange, open, sideOffset = 6, ...rest } = props; 61 | 62 | return ( 63 | 64 | <_SubTrigger> 65 | {icon} 66 | <_ItemLabelGrow>{label} 67 | 68 | 69 | 70 | <_SubContent {...{ sideOffset, ...rest }}>{children} 71 | 72 | 73 | ); 74 | }; 75 | 76 | interface CheckboxItemProps 77 | extends PropsWithChildren, 78 | DropdownMenuPrimitive.DropdownMenuCheckboxItemProps { 79 | icon?: ReactElement; 80 | checkedIcon?: ReactElement; 81 | label: ReactNode; 82 | } 83 | 84 | export const CheckboxItem = (props: CheckboxItemProps) => { 85 | const { label, icon, checked, checkedIcon, ...rest } = props; 86 | 87 | return ( 88 | <_CheckboxItem {...{ checked, ...rest }}> 89 | {(() => { 90 | if (checked) { 91 | return checkedIcon; 92 | } else { 93 | return icon; 94 | } 95 | })()} 96 | <_ItemLabelGrow>{label} 97 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | 104 | interface ItemProps extends DropdownMenuPrimitive.DropdownMenuItemProps { 105 | icon: ReactElement; 106 | shortcut?: string; 107 | label: ReactNode; 108 | } 109 | 110 | export const Item = (props: ItemProps) => { 111 | const { label, icon, shortcut, ...rest } = props; 112 | 113 | return ( 114 | <_Item {...rest}> 115 | {icon} 116 | <_ItemLabelGrow>{label} 117 | {shortcut && <_ShortcutLabel>{shortcut}} 118 | 119 | ); 120 | }; 121 | 122 | interface RootProps 123 | extends PropsWithChildren, 124 | DropdownMenuPrimitive.DropdownMenuContentProps, 125 | DropdownMenuPrimitive.DropdownMenuProps { 126 | trigger: ReactNode; 127 | className?: string; 128 | } 129 | 130 | export const Root = (props: RootProps) => { 131 | const { 132 | className, 133 | onOpenChange, 134 | open, 135 | modal, 136 | defaultOpen, 137 | dir, 138 | sideOffset = 4, 139 | trigger, 140 | children, 141 | ...rest 142 | } = props; 143 | 144 | return ( 145 | 146 | {trigger} 147 | 148 | <_Content {...{ sideOffset, ...rest }}> 149 | <_ContentInner> 150 | <_Arrow /> 151 | {children} 152 | 153 | 154 | 155 | 156 | ); 157 | }; 158 | -------------------------------------------------------------------------------- /components/primitives/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from "class-variance-authority"; 2 | import clsx from "clsx"; 3 | import React, { PropsWithChildren } from "react"; 4 | 5 | const styles = cva("tracking-tight text-primitive-type", { 6 | variants: { 7 | size: { 8 | xxl: "text-4xl sm:text-5xl md:text-7xl lg:text-8xl", 9 | xl: "text-6xl", 10 | lg: "text-3xl", 11 | md: "text-xl", 12 | sm: "text-lg", 13 | xs: "text-base" 14 | }, 15 | weight: { 16 | thin: "font-thin", 17 | extralight: "font-extralight", 18 | light: "font-light", 19 | normal: "font-normal", 20 | medium: "font-medium", 21 | semibold: "font-semibold", 22 | bold: "font-bold", 23 | extrabold: "font-extrabold" 24 | } 25 | }, 26 | defaultVariants: { 27 | size: "md", 28 | weight: "bold" 29 | } 30 | }); 31 | 32 | interface Props extends VariantProps { 33 | as?: HeadingTag; 34 | children?: React.ReactNode; 35 | className?: string; 36 | } 37 | 38 | type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span"; 39 | 40 | export const Heading: React.FC> = (props) => { 41 | const { children, as = "h1", className } = props; 42 | 43 | const Tag = as as HeadingTag; 44 | 45 | return {children}; 46 | }; 47 | -------------------------------------------------------------------------------- /components/primitives/HoverCard.tsx: -------------------------------------------------------------------------------- 1 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 2 | import { PropsWithChildren, ReactNode } from "react"; 3 | import { tw } from "../../utils/tw"; 4 | 5 | const _Content = tw( 6 | HoverCardPrimitive.Content 7 | )`animate-all-sides backdrop-blur bg-opacity-[90%] z-10 border border-primitive-edge max-w-md rounded-lg p-4 md:w-full bg-primitive-faint focus:outline-none focus-visible:ring focus-visible:ring-highlight`; 8 | 9 | const _Arrow = tw(HoverCardPrimitive.Arrow)`fill-current text-primitive-edge`; 10 | 11 | interface RootProps 12 | extends PropsWithChildren, 13 | HoverCardPrimitive.HoverCardProps, 14 | HoverCardPrimitive.HoverCardContentProps { 15 | trigger: ReactNode; 16 | } 17 | 18 | export const Root = (props: RootProps) => { 19 | const { 20 | closeDelay, 21 | openDelay = 100, 22 | open, 23 | onOpenChange, 24 | defaultOpen, 25 | sideOffset = 4, 26 | align = "center", 27 | trigger, 28 | children, 29 | ...rest 30 | } = props; 31 | 32 | return ( 33 | 34 | {trigger} 35 | <_Content {...{ align, sideOffset, ...rest }}> 36 | <_Arrow /> 37 | 38 | {children} 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /components/primitives/Image.tsx: -------------------------------------------------------------------------------- 1 | import { cva } from "class-variance-authority"; 2 | import clsx from "clsx"; 3 | import NextImage, { ImageProps } from "next/image"; 4 | import { useState } from "react"; 5 | 6 | const styles = cva("transition-opacity", { 7 | variants: { 8 | ready: { 9 | true: "opacity-100", 10 | false: "opacity-0" 11 | } 12 | } 13 | }); 14 | 15 | export const Image = (props: ImageProps & { className?: string }) => { 16 | const [ready, setReady] = useState(false); 17 | 18 | const { alt, className, ...rest } = props; 19 | 20 | return ( 21 |
22 | { 26 | setReady(true); 27 | }} 28 | /> 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/primitives/Input.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from "class-variance-authority"; 2 | import clsx from "clsx"; 3 | 4 | export const inputStyles = cva( 5 | "mt-1 block w-full rounded-md focus-visible:border-transparent focus:outline-none focus-visible:ring focus-visible:ring-highlight transition-input", 6 | { 7 | variants: { 8 | rounded: { 9 | md: "rounded-md" 10 | }, 11 | size: { 12 | md: "px-2 py-2" 13 | }, 14 | shade: { 15 | none: "", 16 | primary: "bg-primary text-white hover:bg-primary-bold", 17 | primitive: 18 | "bg-primitive-faint border border-primitive-edge text-primitive-type hover:bg-primitive placeholder:text-primitive-type-faint" 19 | }, 20 | fontSize: { 21 | md: "text-base", 22 | sm: "text-sm", 23 | xs: "text-xs" 24 | }, 25 | fontWeight: { 26 | thin: "font-thin", 27 | extralight: "font-extralight", 28 | light: "font-light", 29 | normal: "font-normal", 30 | medium: "font-medium", 31 | semibold: "font-semibold", 32 | bold: "font-bold", 33 | extrabold: "font-extrabold" 34 | } 35 | }, 36 | defaultVariants: { 37 | fontSize: "sm", 38 | fontWeight: "normal", 39 | size: "md", 40 | rounded: "md", 41 | shade: "primitive" 42 | } 43 | } 44 | ); 45 | 46 | type InputProps = React.ComponentProps<"input"> & VariantProps; 47 | 48 | export const Input = (props: InputProps) => { 49 | const { className, ...rest } = props; 50 | 51 | return ; 52 | }; 53 | -------------------------------------------------------------------------------- /components/primitives/Label.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from "class-variance-authority"; 2 | import clsx from "clsx"; 3 | 4 | export const labelStyles = cva(null, { 5 | variants: { 6 | size: { 7 | lg: "text-lg", 8 | md: "text-md", 9 | sm: "text-sm", 10 | xs: "text-xs" 11 | }, 12 | shade: { 13 | bold: "text-primitive-type-bold", 14 | normal: "text-primitive-type", 15 | faint: "text-primitive-type-faint" 16 | }, 17 | weight: { 18 | thin: "font-thin", 19 | extralight: "font-extralight", 20 | light: "font-light", 21 | normal: "font-normal", 22 | medium: "font-medium", 23 | semibold: "font-semibold", 24 | bold: "font-bold" 25 | } 26 | }, 27 | defaultVariants: { 28 | size: "sm", 29 | weight: "medium", 30 | shade: "faint" 31 | } 32 | }); 33 | 34 | type InputProps = React.ComponentProps<"label"> & VariantProps; 35 | 36 | export const Label = (props: InputProps) => { 37 | const { className, ...rest } = props; 38 | 39 | return