diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index d24fdfc6..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged diff --git a/app/(app)/alpha/additional-details/_client.tsx b/app/(app)/alpha/additional-details/_client.tsx index c8c1d4e6..5c2a664b 100644 --- a/app/(app)/alpha/additional-details/_client.tsx +++ b/app/(app)/alpha/additional-details/_client.tsx @@ -27,18 +27,18 @@ import { slideThreeSubmitAction, slideTwoSubmitAction, } from "./_actions"; -import { Heading, Subheading } from "./components/heading"; -import { Divider } from "./components/divider"; import { ErrorMessage, Field, Fieldset, Label, Legend, -} from "./components/fieldset"; -import { Input } from "./components/input"; -import { Select } from "./components/select"; -import { Button } from "./components/button"; +} from "@/components/ui-components/fieldset"; +import { Input } from "@/components/ui-components/input"; +import { Select } from "@/components/ui-components/select"; +import { Button } from "@/components/ui-components/button"; +import { Heading, Subheading } from "@/components/ui-components/heading"; +import { Divider } from "@/components/ui-components/divider"; type UserDetails = { username: string; diff --git a/components/ui-components/alert.tsx b/components/ui-components/alert.tsx new file mode 100644 index 00000000..de77dc5d --- /dev/null +++ b/components/ui-components/alert.tsx @@ -0,0 +1,108 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React from "react"; +import { Text } from "./text"; + +const sizes = { + xs: "sm:max-w-xs", + sm: "sm:max-w-sm", + md: "sm:max-w-md", + lg: "sm:max-w-lg", + xl: "sm:max-w-xl", + "2xl": "sm:max-w-2xl", + "3xl": "sm:max-w-3xl", + "4xl": "sm:max-w-4xl", + "5xl": "sm:max-w-5xl", +}; + +export function Alert({ + size = "md", + className, + children, + ...props +}: { + size?: keyof typeof sizes; + className?: string; + children: React.ReactNode; +} & Omit) { + return ( + + + +
+
+ + {children} + +
+
+
+ ); +} + +export function AlertTitle({ + className, + ...props +}: { className?: string } & Omit< + Headless.DialogTitleProps, + "as" | "className" +>) { + return ( + + ); +} + +export function AlertDescription({ + className, + ...props +}: { className?: string } & Omit< + Headless.DescriptionProps, + "as" | "className" +>) { + return ( + + ); +} + +export function AlertBody({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return
; +} + +export function AlertActions({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return ( +
+ ); +} diff --git a/components/ui-components/avatar.tsx b/components/ui-components/avatar.tsx new file mode 100644 index 00000000..ae73ff9f --- /dev/null +++ b/components/ui-components/avatar.tsx @@ -0,0 +1,100 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React, { forwardRef } from "react"; +import { TouchTarget } from "./button"; +import { Link } from "./link"; + +type AvatarProps = { + src?: string | null; + square?: boolean; + initials?: string; + alt?: string; + className?: string; +}; + +export function Avatar({ + src = null, + square = false, + initials, + alt = "", + className, + ...props +}: AvatarProps & React.ComponentPropsWithoutRef<"span">) { + return ( + + {initials && ( + + {alt && {alt}} + + {initials} + + + )} + {src && {alt}} + + ); +} + +export const AvatarButton = forwardRef(function AvatarButton( + { + src, + square = false, + initials, + alt, + className, + ...props + }: AvatarProps & + ( + | Omit + | Omit, "className"> + ), + ref: React.ForwardedRef, +) { + const classes = clsx( + className, + square ? "rounded-[20%]" : "rounded-full", + "relative inline-grid focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500", + ); + + return "href" in props ? ( + } + > + + + + + ) : ( + + + + + + ); +}); diff --git a/components/ui-components/badge.tsx b/components/ui-components/badge.tsx new file mode 100644 index 00000000..ca998f2e --- /dev/null +++ b/components/ui-components/badge.tsx @@ -0,0 +1,90 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React, { forwardRef } from "react"; +import { TouchTarget } from "./button"; +import { Link } from "./link"; + +const colors = { + red: "bg-red-500/15 text-red-700 group-data-[hover]:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-[hover]:bg-red-500/20", + orange: + "bg-orange-500/15 text-orange-700 group-data-[hover]:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-[hover]:bg-orange-500/20", + amber: + "bg-amber-400/20 text-amber-700 group-data-[hover]:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-[hover]:bg-amber-400/15", + yellow: + "bg-yellow-400/20 text-yellow-700 group-data-[hover]:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-[hover]:bg-yellow-400/15", + lime: "bg-lime-400/20 text-lime-700 group-data-[hover]:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-[hover]:bg-lime-400/15", + green: + "bg-green-500/15 text-green-700 group-data-[hover]:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-[hover]:bg-green-500/20", + emerald: + "bg-emerald-500/15 text-emerald-700 group-data-[hover]:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-[hover]:bg-emerald-500/20", + teal: "bg-teal-500/15 text-teal-700 group-data-[hover]:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-[hover]:bg-teal-500/20", + cyan: "bg-cyan-400/20 text-cyan-700 group-data-[hover]:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-[hover]:bg-cyan-400/15", + sky: "bg-sky-500/15 text-sky-700 group-data-[hover]:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-[hover]:bg-sky-500/20", + blue: "bg-blue-500/15 text-blue-700 group-data-[hover]:bg-blue-500/25 dark:text-blue-400 dark:group-data-[hover]:bg-blue-500/25", + indigo: + "bg-indigo-500/15 text-indigo-700 group-data-[hover]:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-[hover]:bg-indigo-500/20", + violet: + "bg-violet-500/15 text-violet-700 group-data-[hover]:bg-violet-500/25 dark:text-violet-400 dark:group-data-[hover]:bg-violet-500/20", + purple: + "bg-purple-500/15 text-purple-700 group-data-[hover]:bg-purple-500/25 dark:text-purple-400 dark:group-data-[hover]:bg-purple-500/20", + fuchsia: + "bg-fuchsia-400/15 text-fuchsia-700 group-data-[hover]:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-[hover]:bg-fuchsia-400/20", + pink: "bg-pink-400/15 text-pink-700 group-data-[hover]:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-[hover]:bg-pink-400/20", + rose: "bg-rose-400/15 text-rose-700 group-data-[hover]:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-[hover]:bg-rose-400/20", + zinc: "bg-zinc-600/10 text-zinc-700 group-data-[hover]:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-[hover]:bg-white/10", +}; + +type BadgeProps = { color?: keyof typeof colors }; + +export function Badge({ + color = "zinc", + className, + ...props +}: BadgeProps & React.ComponentPropsWithoutRef<"span">) { + return ( + + ); +} + +export const BadgeButton = forwardRef(function BadgeButton( + { + color = "zinc", + className, + children, + ...props + }: BadgeProps & { className?: string; children: React.ReactNode } & ( + | Omit + | Omit, "className"> + ), + ref: React.ForwardedRef, +) { + const classes = clsx( + className, + "group relative inline-flex rounded-md focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500", + ); + + return "href" in props ? ( + } + > + + {children} + + + ) : ( + + + {children} + + + ); +}); diff --git a/app/(app)/alpha/additional-details/components/button.tsx b/components/ui-components/button.tsx similarity index 100% rename from app/(app)/alpha/additional-details/components/button.tsx rename to components/ui-components/button.tsx diff --git a/components/ui-components/checkbox.tsx b/components/ui-components/checkbox.tsx new file mode 100644 index 00000000..e3dc242d --- /dev/null +++ b/components/ui-components/checkbox.tsx @@ -0,0 +1,160 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React from "react"; + +export function CheckboxGroup({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return ( +
+ ); +} + +export function CheckboxField({ + className, + ...props +}: { className?: string } & Omit) { + return ( + [data-slot=control]]:col-start-1 [&>[data-slot=control]]:row-start-1 [&>[data-slot=control]]:justify-self-center", + // Label layout + "[&>[data-slot=label]]:col-start-2 [&>[data-slot=label]]:row-start-1 [&>[data-slot=label]]:justify-self-start", + // Description layout + "[&>[data-slot=description]]:col-start-2 [&>[data-slot=description]]:row-start-2", + // With description + "[&_[data-slot=label]]:has-[[data-slot=description]]:font-medium", + )} + /> + ); +} + +const base = [ + // Basic layout + "relative isolate flex size-[1.125rem] items-center justify-center rounded-[0.3125rem] sm:size-4", + // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode + "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow", + // Background color when checked + "before:group-data-[checked]:bg-[--checkbox-checked-bg]", + // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo + "dark:before:hidden", + // Background color applied to control in dark mode + "dark:bg-white/5 dark:group-data-[checked]:bg-[--checkbox-checked-bg]", + // Border + "border border-zinc-950/15 group-data-[checked]:border-transparent group-data-[checked]:group-data-[hover]:border-transparent group-data-[hover]:border-zinc-950/30 group-data-[checked]:bg-[--checkbox-checked-border]", + "dark:border-white/15 dark:group-data-[checked]:border-white/5 dark:group-data-[checked]:group-data-[hover]:border-white/5 dark:group-data-[hover]:border-white/30", + // Inner highlight shadow + "after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_theme(colors.white/15%)]", + "dark:after:-inset-px dark:after:hidden dark:after:rounded-[0.3125rem] dark:group-data-[checked]:after:block", + // Focus ring + "group-data-[focus]:outline group-data-[focus]:outline-2 group-data-[focus]:outline-offset-2 group-data-[focus]:outline-blue-500", + // Disabled state + "group-data-[disabled]:opacity-50", + "group-data-[disabled]:border-zinc-950/25 group-data-[disabled]:bg-zinc-950/5 group-data-[disabled]:[--checkbox-check:theme(colors.zinc.950/50%)] group-data-[disabled]:before:bg-transparent", + "dark:group-data-[disabled]:border-white/20 dark:group-data-[disabled]:bg-white/[2.5%] dark:group-data-[disabled]:[--checkbox-check:theme(colors.white/50%)] dark:group-data-[disabled]:group-data-[checked]:after:hidden", + // Forced colors mode + "forced-colors:[--checkbox-check:HighlightText] forced-colors:[--checkbox-checked-bg:Highlight] forced-colors:group-data-[disabled]:[--checkbox-check:Highlight]", + "dark:forced-colors:[--checkbox-check:HighlightText] dark:forced-colors:[--checkbox-checked-bg:Highlight] dark:forced-colors:group-data-[disabled]:[--checkbox-check:Highlight]", +]; + +const colors = { + "dark/zinc": [ + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]", + "dark:[--checkbox-checked-bg:theme(colors.zinc.600)]", + ], + "dark/white": [ + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]", + "dark:[--checkbox-check:theme(colors.zinc.900)] dark:[--checkbox-checked-bg:theme(colors.white)] dark:[--checkbox-checked-border:theme(colors.zinc.950/15%)]", + ], + white: + "[--checkbox-check:theme(colors.zinc.900)] [--checkbox-checked-bg:theme(colors.white)] [--checkbox-checked-border:theme(colors.zinc.950/15%)]", + dark: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.900)] [--checkbox-checked-border:theme(colors.zinc.950/90%)]", + zinc: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.zinc.600)] [--checkbox-checked-border:theme(colors.zinc.700/90%)]", + red: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.red.600)] [--checkbox-checked-border:theme(colors.red.700/90%)]", + orange: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.orange.500)] [--checkbox-checked-border:theme(colors.orange.600/90%)]", + amber: + "[--checkbox-check:theme(colors.amber.950)] [--checkbox-checked-bg:theme(colors.amber.400)] [--checkbox-checked-border:theme(colors.amber.500/80%)]", + yellow: + "[--checkbox-check:theme(colors.yellow.950)] [--checkbox-checked-bg:theme(colors.yellow.300)] [--checkbox-checked-border:theme(colors.yellow.400/80%)]", + lime: "[--checkbox-check:theme(colors.lime.950)] [--checkbox-checked-bg:theme(colors.lime.300)] [--checkbox-checked-border:theme(colors.lime.400/80%)]", + green: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.green.600)] [--checkbox-checked-border:theme(colors.green.700/90%)]", + emerald: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.emerald.600)] [--checkbox-checked-border:theme(colors.emerald.700/90%)]", + teal: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.teal.600)] [--checkbox-checked-border:theme(colors.teal.700/90%)]", + cyan: "[--checkbox-check:theme(colors.cyan.950)] [--checkbox-checked-bg:theme(colors.cyan.300)] [--checkbox-checked-border:theme(colors.cyan.400/80%)]", + sky: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.sky.500)] [--checkbox-checked-border:theme(colors.sky.600/80%)]", + blue: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.blue.600)] [--checkbox-checked-border:theme(colors.blue.700/90%)]", + indigo: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.indigo.500)] [--checkbox-checked-border:theme(colors.indigo.600/90%)]", + violet: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.violet.500)] [--checkbox-checked-border:theme(colors.violet.600/90%)]", + purple: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.purple.500)] [--checkbox-checked-border:theme(colors.purple.600/90%)]", + fuchsia: + "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.fuchsia.500)] [--checkbox-checked-border:theme(colors.fuchsia.600/90%)]", + pink: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.pink.500)] [--checkbox-checked-border:theme(colors.pink.600/90%)]", + rose: "[--checkbox-check:theme(colors.white)] [--checkbox-checked-bg:theme(colors.rose.500)] [--checkbox-checked-border:theme(colors.rose.600/90%)]", +}; + +type Color = keyof typeof colors; + +export function Checkbox({ + color = "dark/zinc", + className, + ...props +}: { + color?: Color; + className?: string; +} & Omit) { + return ( + + + + {/* Checkmark icon */} + + {/* Indeterminate icon */} + + + + + ); +} diff --git a/components/ui-components/description-list.tsx b/components/ui-components/description-list.tsx new file mode 100644 index 00000000..9eb45077 --- /dev/null +++ b/components/ui-components/description-list.tsx @@ -0,0 +1,46 @@ +import clsx from "clsx"; + +export function DescriptionList({ + className, + ...props +}: React.ComponentPropsWithoutRef<"dl">) { + return ( +
+ ); +} + +export function DescriptionTerm({ + className, + ...props +}: React.ComponentPropsWithoutRef<"dt">) { + return ( +
+ ); +} + +export function DescriptionDetails({ + className, + ...props +}: React.ComponentPropsWithoutRef<"dd">) { + return ( +
+ ); +} diff --git a/components/ui-components/dialog.tsx b/components/ui-components/dialog.tsx new file mode 100644 index 00000000..ab9f6fb0 --- /dev/null +++ b/components/ui-components/dialog.tsx @@ -0,0 +1,108 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React from "react"; +import { Text } from "./text"; + +const sizes = { + xs: "sm:max-w-xs", + sm: "sm:max-w-sm", + md: "sm:max-w-md", + lg: "sm:max-w-lg", + xl: "sm:max-w-xl", + "2xl": "sm:max-w-2xl", + "3xl": "sm:max-w-3xl", + "4xl": "sm:max-w-4xl", + "5xl": "sm:max-w-5xl", +}; + +export function Dialog({ + size = "lg", + className, + children, + ...props +}: { + size?: keyof typeof sizes; + className?: string; + children: React.ReactNode; +} & Omit) { + return ( + + + +
+
+ + {children} + +
+
+
+ ); +} + +export function DialogTitle({ + className, + ...props +}: { className?: string } & Omit< + Headless.DialogTitleProps, + "as" | "className" +>) { + return ( + + ); +} + +export function DialogDescription({ + className, + ...props +}: { className?: string } & Omit< + Headless.DescriptionProps, + "as" | "className" +>) { + return ( + + ); +} + +export function DialogBody({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return
; +} + +export function DialogActions({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + return ( +
+ ); +} diff --git a/app/(app)/alpha/additional-details/components/divider.tsx b/components/ui-components/divider.tsx similarity index 100% rename from app/(app)/alpha/additional-details/components/divider.tsx rename to components/ui-components/divider.tsx diff --git a/components/ui-components/dropdown.tsx b/components/ui-components/dropdown.tsx new file mode 100644 index 00000000..cf1cfcf7 --- /dev/null +++ b/components/ui-components/dropdown.tsx @@ -0,0 +1,223 @@ +"use client"; + +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React from "react"; +import { Button } from "./button"; +import { Link } from "./link"; + +export function Dropdown(props: Headless.MenuProps) { + return ; +} + +export function DropdownButton({ + as = Button, + ...props +}: { className?: string } & Omit, "className">) { + return ; +} + +export function DropdownMenu({ + anchor = "bottom", + className, + ...props +}: { className?: string } & Omit) { + return ( + + ); +} + +export function DropdownItem({ + className, + ...props +}: { className?: string } & ( + | Omit, "as" | "className"> + | Omit, "className"> +)) { + const classes = clsx( + className, + // Base styles + "group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-none sm:px-3 sm:py-1.5", + // Text styles + "text-left text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]", + // Focus + "data-[focus]:bg-blue-500 data-[focus]:text-white", + // Disabled state + "data-[disabled]:opacity-50", + // Forced colors mode + "forced-color-adjust-none forced-colors:data-[focus]:bg-[Highlight] forced-colors:data-[focus]:text-[HighlightText] forced-colors:[&>[data-slot=icon]]:data-[focus]:text-[HighlightText]", + // Use subgrid when available but fallback to an explicit grid layout if not + "col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid", + // Icons + "[&>[data-slot=icon]]:col-start-1 [&>[data-slot=icon]]:row-start-1 [&>[data-slot=icon]]:-ml-0.5 [&>[data-slot=icon]]:mr-2.5 [&>[data-slot=icon]]:size-5 sm:[&>[data-slot=icon]]:mr-2 [&>[data-slot=icon]]:sm:size-4", + "[&>[data-slot=icon]]:text-zinc-500 [&>[data-slot=icon]]:data-[focus]:text-white [&>[data-slot=icon]]:dark:text-zinc-400 [&>[data-slot=icon]]:data-[focus]:dark:text-white", + // Avatar + "[&>[data-slot=avatar]]:-ml-1 [&>[data-slot=avatar]]:mr-2.5 [&>[data-slot=avatar]]:size-6 sm:[&>[data-slot=avatar]]:mr-2 sm:[&>[data-slot=avatar]]:size-5", + ); + + return ( + + {"href" in props ? ( + + ) : ( + + + ); +} + +export function PaginationNext({ + href = null, + className, + children = "Next", +}: React.PropsWithChildren<{ href?: string | null; className?: string }>) { + return ( + + + + ); +} + +export function PaginationList({ + className, + ...props +}: React.ComponentPropsWithoutRef<"span">) { + return ( + + ); +} + +export function PaginationPage({ + href, + className, + current = false, + children, +}: React.PropsWithChildren<{ + href: string; + className?: string; + current?: boolean; +}>) { + return ( + + ); +} + +export function PaginationGap({ + className, + children = <>…, + ...props +}: React.ComponentPropsWithoutRef<"span">) { + return ( + + ); +} diff --git a/components/ui-components/radio.tsx b/components/ui-components/radio.tsx new file mode 100644 index 00000000..0bd01729 --- /dev/null +++ b/components/ui-components/radio.tsx @@ -0,0 +1,148 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; + +export function RadioGroup({ + className, + ...props +}: { className?: string } & Omit< + Headless.RadioGroupProps, + "as" | "className" +>) { + return ( + + ); +} + +export function RadioField({ + className, + ...props +}: { className?: string } & Omit) { + return ( + [data-slot=control]]:col-start-1 [&>[data-slot=control]]:row-start-1 [&>[data-slot=control]]:justify-self-center", + // Label layout + "[&>[data-slot=label]]:col-start-2 [&>[data-slot=label]]:row-start-1 [&>[data-slot=label]]:justify-self-start", + // Description layout + "[&>[data-slot=description]]:col-start-2 [&>[data-slot=description]]:row-start-2", + // With description + "[&_[data-slot=label]]:has-[[data-slot=description]]:font-medium", + )} + /> + ); +} + +const base = [ + // Basic layout + "relative isolate flex size-[1.1875rem] shrink-0 rounded-full sm:size-[1.0625rem]", + // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode + "before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-white before:shadow", + // Background color when checked + "before:group-data-[checked]:bg-[--radio-checked-bg]", + // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo + "dark:before:hidden", + // Background color applied to control in dark mode + "dark:bg-white/5 dark:group-data-[checked]:bg-[--radio-checked-bg]", + // Border + "border border-zinc-950/15 group-data-[checked]:border-transparent group-data-[checked]:group-data-[hover]:border-transparent group-data-[hover]:border-zinc-950/30 group-data-[checked]:bg-[--radio-checked-border]", + "dark:border-white/15 dark:group-data-[checked]:border-white/5 dark:group-data-[checked]:group-data-[hover]:border-white/5 dark:group-data-[hover]:border-white/30", + // Inner highlight shadow + "after:absolute after:inset-0 after:rounded-full after:shadow-[inset_0_1px_theme(colors.white/15%)]", + "dark:after:-inset-px dark:after:hidden dark:after:rounded-full dark:group-data-[checked]:after:block", + // Indicator color (light mode) + "[--radio-indicator:transparent] group-data-[checked]:[--radio-indicator:var(--radio-checked-indicator)] group-data-[checked]:group-data-[hover]:[--radio-indicator:var(--radio-checked-indicator)] group-data-[hover]:[--radio-indicator:theme(colors.zinc.900/10%)]", + // Indicator color (dark mode) + "dark:group-data-[checked]:group-data-[hover]:[--radio-indicator:var(--radio-checked-indicator)] dark:group-data-[hover]:[--radio-indicator:theme(colors.zinc.700)]", + // Focus ring + "group-data-[focus]:outline group-data-[focus]:outline-2 group-data-[focus]:outline-offset-2 group-data-[focus]:outline-blue-500", + // Disabled state + "group-data-[disabled]:opacity-50", + "group-data-[disabled]:border-zinc-950/25 group-data-[disabled]:bg-zinc-950/5 group-data-[disabled]:[--radio-checked-indicator:theme(colors.zinc.950/50%)] group-data-[disabled]:before:bg-transparent", + "dark:group-data-[disabled]:border-white/20 dark:group-data-[disabled]:bg-white/[2.5%] dark:group-data-[disabled]:[--radio-checked-indicator:theme(colors.white/50%)] dark:group-data-[disabled]:group-data-[checked]:after:hidden", +]; + +const colors = { + "dark/zinc": [ + "[--radio-checked-bg:theme(colors.zinc.900)] [--radio-checked-border:theme(colors.zinc.950/90%)] [--radio-checked-indicator:theme(colors.white)]", + "dark:[--radio-checked-bg:theme(colors.zinc.600)]", + ], + "dark/white": [ + "[--radio-checked-bg:theme(colors.zinc.900)] [--radio-checked-border:theme(colors.zinc.950/90%)] [--radio-checked-indicator:theme(colors.white)]", + "dark:[--radio-checked-bg:theme(colors.white)] dark:[--radio-checked-border:theme(colors.zinc.950/15%)] dark:[--radio-checked-indicator:theme(colors.zinc.900)]", + ], + white: + "[--radio-checked-bg:theme(colors.white)] [--radio-checked-border:theme(colors.zinc.950/15%)] [--radio-checked-indicator:theme(colors.zinc.900)]", + dark: "[--radio-checked-bg:theme(colors.zinc.900)] [--radio-checked-border:theme(colors.zinc.950/90%)] [--radio-checked-indicator:theme(colors.white)]", + zinc: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.zinc.600)] [--radio-checked-border:theme(colors.zinc.700/90%)]", + red: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.red.600)] [--radio-checked-border:theme(colors.red.700/90%)]", + orange: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.orange.500)] [--radio-checked-border:theme(colors.orange.600/90%)]", + amber: + "[--radio-checked-bg:theme(colors.amber.400)] [--radio-checked-border:theme(colors.amber.500/80%)] [--radio-checked-indicator:theme(colors.amber.950)]", + yellow: + "[--radio-checked-bg:theme(colors.yellow.300)] [--radio-checked-border:theme(colors.yellow.400/80%)] [--radio-checked-indicator:theme(colors.yellow.950)]", + lime: "[--radio-checked-bg:theme(colors.lime.300)] [--radio-checked-border:theme(colors.lime.400/80%)] [--radio-checked-indicator:theme(colors.lime.950)]", + green: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.green.600)] [--radio-checked-border:theme(colors.green.700/90%)]", + emerald: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.emerald.600)] [--radio-checked-border:theme(colors.emerald.700/90%)]", + teal: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.teal.600)] [--radio-checked-border:theme(colors.teal.700/90%)]", + cyan: "[--radio-checked-bg:theme(colors.cyan.300)] [--radio-checked-border:theme(colors.cyan.400/80%)] [--radio-checked-indicator:theme(colors.cyan.950)]", + sky: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.sky.500)] [--radio-checked-border:theme(colors.sky.600/80%)]", + blue: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.blue.600)] [--radio-checked-border:theme(colors.blue.700/90%)]", + indigo: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.indigo.500)] [--radio-checked-border:theme(colors.indigo.600/90%)]", + violet: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.violet.500)] [--radio-checked-border:theme(colors.violet.600/90%)]", + purple: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.purple.500)] [--radio-checked-border:theme(colors.purple.600/90%)]", + fuchsia: + "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.fuchsia.500)] [--radio-checked-border:theme(colors.fuchsia.600/90%)]", + pink: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.pink.500)] [--radio-checked-border:theme(colors.pink.600/90%)]", + rose: "[--radio-checked-indicator:theme(colors.white)] [--radio-checked-bg:theme(colors.rose.500)] [--radio-checked-border:theme(colors.rose.600/90%)]", +}; + +type Color = keyof typeof colors; + +export function Radio({ + color = "dark/zinc", + className, + ...props +}: { color?: Color; className?: string } & Omit< + Headless.RadioProps, + "as" | "className" | "children" +>) { + return ( + + + + + + ); +} diff --git a/app/(app)/alpha/additional-details/components/select.tsx b/components/ui-components/select.tsx similarity index 100% rename from app/(app)/alpha/additional-details/components/select.tsx rename to components/ui-components/select.tsx diff --git a/components/ui-components/sidebar-layout.tsx b/components/ui-components/sidebar-layout.tsx new file mode 100644 index 00000000..f3e9f1bc --- /dev/null +++ b/components/ui-components/sidebar-layout.tsx @@ -0,0 +1,92 @@ +"use client"; + +import * as Headless from "@headlessui/react"; +import React, { useState } from "react"; +import { NavbarItem } from "./navbar"; + +function OpenMenuIcon() { + return ( + + ); +} + +function CloseMenuIcon() { + return ( + + ); +} + +function MobileSidebar({ + open, + close, + children, +}: React.PropsWithChildren<{ open: boolean; close: () => void }>) { + return ( + + + +
+
+ + + +
+ {children} +
+
+
+ ); +} + +export function SidebarLayout({ + navbar, + sidebar, + children, +}: React.PropsWithChildren<{ + navbar: React.ReactNode; + sidebar: React.ReactNode; +}>) { + const [showSidebar, setShowSidebar] = useState(false); + + return ( +
+ {/* Sidebar on desktop */} +
{sidebar}
+ + {/* Sidebar on mobile */} + setShowSidebar(false)}> + {sidebar} + + + {/* Navbar on mobile */} +
+
+ setShowSidebar(true)} + aria-label="Open navigation" + > + + +
+
{navbar}
+
+ + {/* Content */} +
+
+
{children}
+
+
+
+ ); +} diff --git a/components/ui-components/sidebar.tsx b/components/ui-components/sidebar.tsx new file mode 100644 index 00000000..953ec1ac --- /dev/null +++ b/components/ui-components/sidebar.tsx @@ -0,0 +1,199 @@ +"use client"; +/* eslint-disable jsx-a11y/heading-has-content */ + +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import { LayoutGroup, motion } from "framer-motion"; +import React, { Fragment, forwardRef, useId } from "react"; +import { TouchTarget } from "./button"; +import { Link } from "./link"; + +export function Sidebar({ + className, + ...props +}: React.ComponentPropsWithoutRef<"nav">) { + return ( +