more tweaks to layout

This commit is contained in:
miloschwartz 2025-04-13 14:21:18 -04:00
parent b731a50cc9
commit 8b0c30f19f
No known key found for this signature in database
22 changed files with 172 additions and 193 deletions

View file

@ -45,7 +45,7 @@ export default async function OrgPage(props: OrgPageProps) {
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout <Layout
orgId={orgId} orgId={orgId}
navItems={orgNavItems} navItems={orgNavItems}
> >

View file

@ -14,6 +14,7 @@ import {
} from "@app/components/ui/breadcrumb"; } from "@app/components/ui/breadcrumb";
import Link from "next/link"; import Link from "next/link";
import { cache } from "react"; import { cache } from "react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
interface UserLayoutProps { interface UserLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -48,28 +49,11 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
return ( return (
<> <>
<SettingsSectionTitle
title={`${user?.email}`}
description="Manage the settings on this user"
/>
<OrgUserProvider orgUser={user}> <OrgUserProvider orgUser={user}>
<div className="mb-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<Link href="../../">Users</Link>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{user.email}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="space-y-0.5 mb-6">
<h2 className="text-2xl font-bold tracking-tight">
User {user?.email}
</h2>
<p className="text-muted-foreground">Manage user</p>
</div>
<HorizontalTabs items={navItems}> <HorizontalTabs items={navItems}>
{children} {children}
</HorizontalTabs> </HorizontalTabs>

View file

@ -18,6 +18,7 @@ import { GetOrgUserResponse } from "@server/routers/user";
import UserProvider from "@app/providers/UserProvider"; import UserProvider from "@app/providers/UserProvider";
import { Layout } from "@app/components/Layout"; import { Layout } from "@app/components/Layout";
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav"; import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
import { orgNavItems } from "@app/app/navigation";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@ -26,51 +27,6 @@ export const metadata: Metadata = {
description: "" description: ""
}; };
const navItems: SidebarNavItem[] = [
{
title: "Sites",
href: "/{orgId}/settings/sites"
// icon: <Combine className="h-4 w-4" />
},
{
title: "Resources",
href: "/{orgId}/settings/resources"
// icon: <Waypoints className="h-4 w-4" />
},
{
title: "Access Control",
href: "/{orgId}/settings/access",
// icon: <Users className="h-4 w-4" />,
autoExpand: true,
children: [
{
title: "Users",
href: "/{orgId}/settings/access/users",
children: [
{
title: "Invitations",
href: "/{orgId}/settings/access/invitations"
}
]
},
{
title: "Roles",
href: "/{orgId}/settings/access/roles"
}
]
},
{
title: "Shareable Links",
href: "/{orgId}/settings/share-links"
// icon: <LinkIcon className="h-4 w-4" />
},
{
title: "General",
href: "/{orgId}/settings/general"
// icon: <Settings className="h-4 w-4" />
}
];
interface SettingsLayoutProps { interface SettingsLayoutProps {
children: React.ReactNode; children: React.ReactNode;
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@ -119,7 +75,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout orgId={params.orgId} orgs={orgs} navItems={navItems}> <Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}>
{children} {children}
</Layout> </Layout>
</UserProvider> </UserProvider>

View file

@ -569,7 +569,7 @@ export default function ReverseProxyTargets(props: {
<Button <Button
type="submit" type="submit"
variant="outlinePrimary" variant="outlinePrimary"
className="mt-8" className="mt-6"
> >
Add Target Add Target
</Button> </Button>

View file

@ -693,7 +693,7 @@ export default function ResourceRules(props: {
control={addRuleForm.control} control={addRuleForm.control}
name="value" name="value"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="space-y-0 mb-2">
<InfoPopup <InfoPopup
text="Value" text="Value"
info={ info={
@ -714,6 +714,7 @@ export default function ResourceRules(props: {
<Button <Button
type="submit" type="submit"
variant="outlinePrimary" variant="outlinePrimary"
className="mb-2"
disabled={!rulesEnabled} disabled={!rulesEnabled}
> >
Add Rule Add Rule

View file

@ -9,6 +9,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout"; import { Layout } from "@app/components/Layout";
import { adminNavItems } from "../navigation";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@ -17,14 +18,6 @@ export const metadata: Metadata = {
description: "" description: ""
}; };
const navItems = [
{
title: "All Users",
href: "/admin/users",
icon: <Users className="h-4 w-4" />
}
];
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
@ -51,7 +44,7 @@ export default async function AdminLayout(props: LayoutProps) {
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout orgs={orgs} navItems={navItems}> <Layout orgs={orgs} navItems={adminNavItems}>
{props.children} {props.children}
</Layout> </Layout>
</UserProvider> </UserProvider>

View file

@ -1,3 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&display=swap");
@import 'tailwindcss'; @import 'tailwindcss';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));

View file

@ -1,33 +1,37 @@
import { Home, Settings, Users, Link as LinkIcon, Waypoints, Combine } from "lucide-react"; import { SidebarNavItem } from "@app/components/SidebarNav";
import {
Home,
Settings,
Users,
Link as LinkIcon,
Waypoints,
Combine
} from "lucide-react";
export const rootNavItems = [ export const rootNavItems: SidebarNavItem[] = [
{ {
title: "Home", title: "Home",
href: "/", href: "/"
icon: <Home className="h-4 w-4" /> // icon: <Home className="h-4 w-4" />
} }
]; ];
export const orgNavItems = [ export const orgNavItems: SidebarNavItem[] = [
{
title: "Overview",
href: "/{orgId}",
icon: <Home className="h-4 w-4" />
},
{ {
title: "Sites", title: "Sites",
href: "/{orgId}/settings/sites", href: "/{orgId}/settings/sites"
icon: <Combine className="h-4 w-4" /> // icon: <Combine className="h-4 w-4" />
}, },
{ {
title: "Resources", title: "Resources",
href: "/{orgId}/settings/resources", href: "/{orgId}/settings/resources"
icon: <Waypoints className="h-4 w-4" /> // icon: <Waypoints className="h-4 w-4" />
}, },
{ {
title: "Access Control", title: "Access Control",
href: "/{orgId}/settings/access", href: "/{orgId}/settings/access",
icon: <Users className="h-4 w-4" />, // icon: <Users className="h-4 w-4" />,
autoExpand: true,
children: [ children: [
{ {
title: "Users", title: "Users",
@ -41,12 +45,20 @@ export const orgNavItems = [
}, },
{ {
title: "Shareable Links", title: "Shareable Links",
href: "/{orgId}/settings/share-links", href: "/{orgId}/settings/share-links"
icon: <LinkIcon className="h-4 w-4" /> // icon: <LinkIcon className="h-4 w-4" />
}, },
{ {
title: "Settings", title: "Settings",
href: "/{orgId}/settings/general", href: "/{orgId}/settings/general"
icon: <Settings className="h-4 w-4" /> // icon: <Settings className="h-4 w-4" />
} }
]; ];
export const adminNavItems: SidebarNavItem[] = [
{
title: "All Users",
href: "/admin/users"
// icon: <Users className="h-4 w-4" />
}
];

View file

@ -71,7 +71,7 @@ export default async function Page(props: {
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout <Layout
orgs={orgs} orgs={orgs}
navItems={rootNavItems} navItems={rootNavItems}
showBreadcrumbs={false} showBreadcrumbs={false}

View file

@ -1,3 +1,4 @@
import { Layout } from "@app/components/Layout";
import ProfileIcon from "@app/components/ProfileIcon"; import ProfileIcon from "@app/components/ProfileIcon";
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
@ -5,6 +6,7 @@ import UserProvider from "@app/providers/UserProvider";
import { Metadata } from "next"; import { Metadata } from "next";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react"; import { cache } from "react";
import { rootNavItems } from "../navigation";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Setup - Pangolin`, title: `Setup - Pangolin`,
@ -27,27 +29,19 @@ export default async function SetupLayout({
redirect("/?redirect=/setup"); redirect("/?redirect=/setup");
} }
if ( if (!(!env.flags.disableUserCreateOrg || user.serverAdmin)) {
!(!env.flags.disableUserCreateOrg || user.serverAdmin)
) {
redirect("/"); redirect("/");
} }
return ( return (
<> <>
<div className="p-3"> <UserProvider user={user}>
{user && ( <Layout navItems={rootNavItems} showBreadcrumbs={false}>
<UserProvider user={user}> <div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
<div> {children}
<ProfileIcon /> </div>
</div> </Layout>
</UserProvider> </UserProvider>
)}
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
{children}
</div>
</div>
</> </>
); );
} }

View file

@ -26,7 +26,7 @@ export function Breadcrumbs() {
} else if (segment === "resources") { } else if (segment === "resources") {
label = "Resources"; label = "Resources";
} else if (segment === "access") { } else if (segment === "access") {
label = "Users & Roles"; label = "Access Control";
} else if (segment === "general") { } else if (segment === "general") {
label = "General"; label = "General";
} else if (segment === "share-links") { } else if (segment === "share-links") {
@ -48,14 +48,14 @@ export function Breadcrumbs() {
return ( return (
<div className="border-b px-4 py-2 overflow-x-auto scrollbar-hide bg-card"> <div className="border-b px-4 py-2 overflow-x-auto scrollbar-hide bg-card">
<nav className="flex items-center space-x-1 text-sm text-muted-foreground"> <nav className="flex items-center space-x-1 text-sm text-muted-foreground whitespace-nowrap">
{breadcrumbs.map((crumb, index) => ( {breadcrumbs.map((crumb, index) => (
<div key={crumb.href} className="flex items-center"> <div key={crumb.href} className="flex items-center whitespace-nowrap">
{index !== 0 && <ChevronRight className="h-4 w-4" />} {index !== 0 && <ChevronRight className="h-4 w-4" />}
<Link <Link
href={crumb.href} href={crumb.href}
className={cn( className={cn(
"ml-1 hover:text-foreground", "ml-1 hover:text-foreground whitespace-nowrap",
index === breadcrumbs.length - 1 && index === breadcrumbs.length - 1 &&
"text-foreground font-medium" "text-foreground font-medium"
)} )}

View file

@ -21,7 +21,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
width={34} width={34}
height={34} height={34}
/> />
<span className="font-bold text-2xl">Pangolin</span> <span className="font-[Space_Grotesk] font-bold text-2xl text-neutral-500">Pangolin</span>
</Link> </Link>
</div> </div>
); );

View file

@ -28,7 +28,8 @@ export function HorizontalTabs({
return href return href
.replace("{orgId}", params.orgId as string) .replace("{orgId}", params.orgId as string)
.replace("{resourceId}", params.resourceId as string) .replace("{resourceId}", params.resourceId as string)
.replace("{niceId}", params.niceId as string); .replace("{niceId}", params.niceId as string)
.replace("{userId}", params.userId as string);
} }
return ( return (

View file

@ -10,7 +10,7 @@ import { ListOrgsResponse } from "@server/routers/org";
import SupporterStatus from "@app/components/SupporterStatus"; import SupporterStatus from "@app/components/SupporterStatus";
import { Separator } from "@app/components/ui/separator"; import { Separator } from "@app/components/ui/separator";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { Menu, X } from "lucide-react"; import { ExternalLink, Menu, X } from "lucide-react";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@ -20,6 +20,7 @@ import {
} from "@app/components/ui/sheet"; } from "@app/components/ui/sheet";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Breadcrumbs } from "@app/components/Breadcrumbs"; import { Breadcrumbs } from "@app/components/Breadcrumbs";
import Link from "next/link";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -68,7 +69,10 @@ export function Layout({
<Menu className="h-6 w-6" /> <Menu className="h-6 w-6" />
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="left" className="w-64 p-0 flex flex-col h-full"> <SheetContent
side="left"
className="w-64 p-0 flex flex-col h-full"
>
<SheetTitle className="sr-only"> <SheetTitle className="sr-only">
Navigation Menu Navigation Menu
</SheetTitle> </SheetTitle>
@ -81,7 +85,7 @@ export function Layout({
</div> </div>
)} )}
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
<SidebarNav items={navItems} /> <SidebarNav items={navItems} onItemClick={() => setIsMobileMenuOpen(false)} />
</div> </div>
<div className="p-4 space-y-4 border-t shrink-0"> <div className="p-4 space-y-4 border-t shrink-0">
<SupporterStatus /> <SupporterStatus />
@ -113,7 +117,15 @@ export function Layout({
<OrgSelector orgId={orgId} orgs={orgs} /> <OrgSelector orgId={orgId} orgs={orgs} />
<div className="space-y-2"> <div className="space-y-2">
<div className="text-xs text-muted-foreground text-center"> <div className="text-xs text-muted-foreground text-center">
Open Source <Link
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
Open Source
<ExternalLink size={12}/>
</Link>
</div> </div>
{env?.app?.version && ( {env?.app?.version && (
<div className="text-xs text-muted-foreground text-center"> <div className="text-xs text-muted-foreground text-center">
@ -126,7 +138,12 @@ export function Layout({
)} )}
{/* Main content */} {/* Main content */}
<div className={cn("flex-1 flex flex-col h-full min-w-0", !showSidebar && "w-full")}> <div
className={cn(
"flex-1 flex flex-col h-full min-w-0",
!showSidebar && "w-full"
)}
>
{showTopBar && ( {showTopBar && (
<div className="h-16 border-b shrink-0 bg-card"> <div className="h-16 border-b shrink-0 bg-card">
<div className="flex h-full items-center justify-end px-4"> <div className="flex h-full items-center justify-end px-4">
@ -135,7 +152,7 @@ export function Layout({
</div> </div>
)} )}
{showBreadcrumbs && <Breadcrumbs />} {showBreadcrumbs && <Breadcrumbs />}
<main className="flex-1 overflow-y-auto p-6 w-full"> <main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
{children} {children}
</div> </div>

View file

@ -17,12 +17,14 @@ export interface SidebarNavItem {
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> { export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: SidebarNavItem[]; items: SidebarNavItem[];
disabled?: boolean; disabled?: boolean;
onItemClick?: () => void;
} }
export function SidebarNav({ export function SidebarNav({
className, className,
items, items,
disabled = false, disabled = false,
onItemClick,
...props ...props
}: SidebarNavProps) { }: SidebarNavProps) {
const pathname = usePathname(); const pathname = usePathname();
@ -87,13 +89,19 @@ export function SidebarNav({
<Link <Link
href={hydratedHref} href={hydratedHref}
className={cn( className={cn(
"flex items-center py-2 px-3 w-full transition-colors", "flex items-center py-1 w-full transition-colors",
isActive isActive
? "text-primary font-medium" ? "text-primary font-medium"
: "text-muted-foreground hover:text-foreground", : "text-muted-foreground hover:text-foreground",
disabled && "cursor-not-allowed opacity-60" disabled && "cursor-not-allowed opacity-60"
)} )}
onClick={disabled ? (e) => e.preventDefault() : undefined} onClick={(e) => {
if (disabled) {
e.preventDefault();
} else if (onItemClick) {
onItemClick();
}
}}
tabIndex={disabled ? -1 : undefined} tabIndex={disabled ? -1 : undefined}
aria-disabled={disabled} aria-disabled={disabled}
> >

View file

@ -21,7 +21,7 @@ export function TopBar({ orgId, orgs }: TopBarProps) {
Documentation Documentation
</Link> </Link>
<Link <Link
href="https://fossorial.io/support" href="mailto:support@fossorial.io"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors" className="text-muted-foreground hover:text-foreground transition-colors"

View file

@ -221,7 +221,7 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
modal={usePortal} modal={usePortal}
> >
<div <div
className="relative h-full flex items-center rounded-md border bg-transparent pr-3" className="relative h-full flex items-center rounded-md border border-input bg-transparent pr-3"
ref={triggerContainerRef} ref={triggerContainerRef}
> >
{childrenWithProps} {childrenWithProps}

View file

@ -6,7 +6,7 @@ import { cn } from "@app/lib/cn";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center rounded-full whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "cursor-pointer inline-flex items-center justify-center rounded-full whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {

View file

@ -24,7 +24,12 @@ import { useState } from "react";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { DataTablePagination } from "@app/components/DataTablePagination"; import { DataTablePagination } from "@app/components/DataTablePagination";
import { Plus, Search } from "lucide-react"; import { Plus, Search } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@app/components/ui/card"; import {
Card,
CardContent,
CardHeader,
CardTitle
} from "@app/components/ui/card";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
@ -47,6 +52,7 @@ export function DataTable<TData, TValue>({
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState<any>([]);
const table = useReactTable({ const table = useReactTable({
data, data,
@ -57,6 +63,7 @@ export function DataTable<TData, TValue>({
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onGlobalFilterChange: setGlobalFilter,
initialState: { initialState: {
pagination: { pagination: {
pageSize: 20, pageSize: 20,
@ -65,7 +72,8 @@ export function DataTable<TData, TValue>({
}, },
state: { state: {
sorting, sorting,
columnFilters columnFilters,
globalFilter
} }
}); });
@ -73,12 +81,12 @@ export function DataTable<TData, TValue>({
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div className="flex items-center max-w-sm w-full relative"> <div className="flex items-center max-w-sm w-full relative mr-2">
<Input <Input
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
value={(table.getColumn(searchColumn)?.getFilterValue() as string) ?? ""} value={globalFilter ?? ""}
onChange={(event) => onChange={(e) =>
table.getColumn(searchColumn)?.setFilterValue(event.target.value) table.setGlobalFilter(String(e.target.value))
} }
className="w-full pl-8" className="w-full pl-8"
/> />
@ -101,7 +109,8 @@ export function DataTable<TData, TValue>({
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef
.header,
header.getContext() header.getContext()
)} )}
</TableHead> </TableHead>
@ -114,7 +123,9 @@ export function DataTable<TData, TValue>({
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={
row.getIsSelected() && "selected"
}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>

View file

@ -1,32 +1,32 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
FieldPath, FieldPath,
FieldValues, FieldValues,
FormProvider, FormProvider,
useFormContext, useFormContext
} from "react-hook-form" } from "react-hook-form";
import { cn } from "@app/lib/cn" import { cn } from "@app/lib/cn";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -38,21 +38,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -60,37 +60,37 @@ const useFormField = () => {
formItemId: `${id}-form-item`, formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue
) );
const FormItem = React.forwardRef< const FormItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
}) });
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
@ -99,15 +99,16 @@ const FormLabel = React.forwardRef<
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<Slot <Slot
@ -121,15 +122,15 @@ const FormControl = React.forwardRef<
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
}) });
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
@ -138,19 +139,19 @@ const FormDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
) );
}) });
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
@ -162,9 +163,9 @@ const FormMessage = React.forwardRef<
> >
{body} {body}
</p> </p>
) );
}) });
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage";
export { export {
useFormField, useFormField,
@ -174,5 +175,5 @@ export {
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField
} };

View file

@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 items-center justify-center border-y-2 border-r-2 border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l-2 last:rounded-r-md", "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background", isActive && "z-10 ring-2 ring-ring ring-offset-background",
className className
)} )}

View file

@ -19,7 +19,7 @@ const Switch = React.forwardRef<
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5"
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>