Migrate UI to COSS components + COSS neutral theme; add i18next

Components:
- Replace components/ui primitives with COSS equivalents from the @coss/* shadcn
  registry. Add menu/preview-card/group; remove the superseded dropdown-menu,
  hover-card, button-group; keep carousel (no COSS equivalent).
- Migrate live call sites to canonical COSS APIs preserving layout/behavior:
  sidebar menus (nav-user, team-switcher, nav-notifications), chat-input radio
  menu, patient dialogs/cards, auth forms (FieldGroup -> flex column), settings.
- ai-elements: migrate the live conversation/message with behavior parity
  (Tooltip via render, Group/GroupText); repoint dormant files (hover-card ->
  preview-card, dropdown-menu -> menu, button-group -> group, InputGroupButton
  -> Button). Residual pre-existing Base UI type drift stays behind
  ignoreBuildErrors.

Theme:
- Adopt COSS default neutral tokens in globals.css with light + dark palettes.
- Add next-themes (defaultTheme=dark, enableSystem) and drop the forced dark
  class. Align font variables to the COSS contract (--font-sans/-heading/-mono).
  Make scrollbars theme-aware.

i18n:
- Add i18next + react-i18next (lib/i18n/config.ts, locales/en/translation.json,
  components/i18n-provider.tsx mounted in layout). Convert auth forms, sidebar
  nav, and settings tabs to useTranslation() as the reference pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-03 18:39:20 +03:00
parent 26e933fb6c
commit 4d6a5dc008
63 changed files with 3729 additions and 2545 deletions
+29 -11
View File
@@ -64,28 +64,46 @@ trailer. See the root `../CLAUDE.md` for the project-wide per-folder commit poli
- **Stack:** Next.js 16.2.6 (App Router) · React 19 · TypeScript · Tailwind CSS **v4**. Path alias
`@/*` → repo root (`tsconfig.json`). `cn()` (clsx + tailwind-merge) lives in `lib/utils.ts`.
- **`app/`** — App Router. `app/page.tsx` is the product chat page (sidebar + chat panel);
`app/layout.tsx` **forces dark mode** by putting `dark` on `<html>` and loads the fonts.
- **`components/ui/`** — shadcn components, but built on **Base UI (`@base-ui/react`), not Radix.**
APIs differ: composition uses the `render` prop and `useRender`/`mergeProps`, not `asChild`.
Match existing files when adding/editing primitives. Config in `components.json` (style
`base-luma`, baseColor `neutral`).
- **`app/`** — App Router. `app/(app)/page.tsx` is the product chat page (sidebar + chat panel);
`app/layout.tsx` loads the fonts and wraps children in `ThemeProvider` (next-themes) +
`I18nProvider` (see Theming and i18n below).
- **`components/ui/`** — **COSS components** (built on **Base UI `@base-ui/react`, not Radix**),
installed via the shadcn CLI from the `@coss/*` registry. APIs follow Base UI: composition uses
the `render` prop and `useRender`/`mergeProps`, **not `asChild`**, and popups use canonical COSS
names (`DialogPopup`/`DialogPanel`, `MenuPopup`, `SelectPopup`, `TooltipPopup`, `PreviewCard`,
`Group`) — COSS also ships back-compat aliases (`DialogContent`, `CardContent`, `SelectContent`,
`TooltipContent`, …). Add/update primitives with `npx shadcn@latest add @coss/<name>`.
`carousel.tsx` is the one **non-COSS** primitive kept (no COSS equivalent). Config in
`components.json` (baseColor `neutral`).
- **`components/ai-elements/`** — a large AI-chat primitive library (`PromptInput`, `Conversation`,
`Message`, `Suggestion`, etc.) typed against **AI SDK v6** (`ai` package: `UIMessage`,
`ChatStatus`, `FileUIPart`). Note: `@ai-sdk/react` (`useChat`) is **not installed** — chat state
is managed with local React state.
- **`components/sidebar-02/`** — the dashboard sidebar (`SidebarProvider` / `Sidebar` /
`SidebarInset` from `components/ui/sidebar.tsx`). `app-sidebar.tsx` holds the nav config (New chat
· Patients · Settings) + notifications; `team-switcher.tsx` exists but is no longer used.
· Patients · Settings) + notifications; `team-switcher.tsx` is the `OrgSwitcher` (clinic switch).
- **`components/chat/`** — the product chat UI. `chat-panel.tsx` owns message state + empty/active
layouts; `chat-input.tsx` is a bespoke (nonai-elements) input matching a specific design.
## Theming
Tailwind v4 with `@theme inline` and **oklch CSS variables** in `app/globals.css` (a
Linear-inspired dark palette; `--primary` is indigo `#5e6ad2`). The app is dark-only. The radius
scale (`rounded-2xl``rounded-4xl`) is derived from `--radius`, so those utilities are larger than
stock Tailwind.
Tailwind v4 with `@theme inline` in `app/globals.css`, using **COSS's default neutral tokens**
(`@coss/colors-neutral`; values reference Tailwind palette vars like `--color-neutral-*` plus
`--alpha()`/`color-mix()`). Both **light (`:root`) and dark (`.dark`)** palettes are defined;
`next-themes` (`components/theme-provider.tsx`) toggles them with **`defaultTheme="dark"`** +
`enableSystem`, so the app still defaults to dark. Fonts follow the COSS variable contract
(`--font-sans`, `--font-heading` = Inter; `--font-mono` = Geist Mono). The radius scale
(`rounded-2xl``rounded-4xl`) is derived from `--radius`, so those utilities are larger than stock
Tailwind.
## i18n
`i18next` + `react-i18next` (config in `lib/i18n/config.ts`, English resources in
`lib/i18n/locales/en/translation.json`). `components/i18n-provider.tsx` wraps the app in
`app/layout.tsx`. Use `const { t } = useTranslation()` + nested keys (e.g. `t("auth.login.title")`)
in **client** components. To add a language, drop a `locales/<lng>/translation.json` and register it
in `resources`/`supportedLngs` in `config.ts`. Auth forms, sidebar nav, and settings tabs are
converted as the reference pattern; other strings can be migrated incrementally.
## Gotchas
+85 -58
View File
@@ -8,8 +8,8 @@
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--font-mono: var(--font-mono);
--font-heading: var(--font-heading);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -46,83 +46,110 @@
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--color-warning-foreground: var(--warning-foreground);
--color-warning: var(--warning);
--color-success-foreground: var(--success-foreground);
--color-success: var(--success);
--color-info-foreground: var(--info-foreground);
--color-info: var(--info);
--color-destructive-foreground: var(--destructive-foreground);
--animate-skeleton: skeleton 2s -1s infinite linear;
@keyframes skeleton {
to {
background-position: -200% 0;
}
}
}
:root {
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.488 0.243 264.376);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--card: var(--color-white);
--card-foreground: var(--color-neutral-800);
--popover: var(--color-white);
--popover-foreground: var(--color-neutral-800);
--primary: var(--color-neutral-800);
--primary-foreground: var(--color-neutral-50);
--secondary: --alpha(var(--color-black) / 4%);
--secondary-foreground: var(--color-neutral-800);
--muted: --alpha(var(--color-black) / 4%);
--muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));
--accent: --alpha(var(--color-black) / 4%);
--accent-foreground: var(--color-neutral-800);
--destructive: var(--color-red-500);
--border: --alpha(var(--color-black) / 8%);
--input: --alpha(var(--color-black) / 10%);
--ring: var(--color-neutral-400);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--radius: 0.875rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--sidebar: var(--color-neutral-50);
--sidebar-foreground: var(--color-neutral-800);
--sidebar-primary: var(--color-neutral-800);
--sidebar-primary-foreground: var(--color-neutral-50);
--sidebar-accent: --alpha(var(--color-black) / 4%);
--sidebar-accent-foreground: var(--color-neutral-800);
--sidebar-border: --alpha(var(--color-black) / 8%);
--sidebar-ring: var(--color-neutral-400);
--background: var(--color-white);
--foreground: var(--color-neutral-800);
--destructive-foreground: var(--color-red-700);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-700);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
}
/* Linear-inspired dark palette: near-black canvas, #5e6ad2 indigo accent, hairline borders. */
/* COSS neutral dark palette: near-black canvas, neutral accent, hairline borders. */
.dark {
--background: oklch(0.172 0.004 277);
--foreground: oklch(0.98 0.002 277);
--card: oklch(0.193 0.005 277);
--card-foreground: oklch(0.98 0.002 277);
--popover: oklch(0.193 0.005 277);
--popover-foreground: oklch(0.98 0.002 277);
--primary: oklch(0.556 0.155 277);
--primary-foreground: oklch(0.985 0.001 277);
--secondary: oklch(0.225 0.006 277);
--secondary-foreground: oklch(0.98 0.002 277);
--muted: oklch(0.225 0.006 277);
--muted-foreground: oklch(0.652 0.012 268);
--accent: oklch(0.245 0.008 277);
--accent-foreground: oklch(0.98 0.002 277);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 8%);
--input: oklch(1 0 0 / 10%);
--ring: oklch(0.556 0.155 277);
--background: color-mix(in srgb, var(--color-neutral-950) 95%, var(--color-white));
--foreground: var(--color-neutral-100);
--card: color-mix(in srgb, var(--background) 98%, var(--color-white));
--card-foreground: var(--color-neutral-100);
--popover: color-mix(in srgb, var(--background) 98%, var(--color-white));
--popover-foreground: var(--color-neutral-100);
--primary: var(--color-neutral-100);
--primary-foreground: var(--color-neutral-800);
--secondary: --alpha(var(--color-white) / 4%);
--secondary-foreground: var(--color-neutral-100);
--muted: --alpha(var(--color-white) / 4%);
--muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));
--accent: --alpha(var(--color-white) / 4%);
--accent-foreground: var(--color-neutral-100);
--destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
--border: --alpha(var(--color-white) / 6%);
--input: --alpha(var(--color-white) / 8%);
--ring: var(--color-neutral-500);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.138 0.004 277);
--sidebar-foreground: oklch(0.98 0.002 277);
--sidebar-primary: oklch(0.556 0.155 277);
--sidebar-primary-foreground: oklch(0.985 0.001 277);
--sidebar-accent: oklch(0.245 0.008 277);
--sidebar-accent-foreground: oklch(0.98 0.002 277);
--sidebar-border: oklch(1 0 0 / 8%);
--sidebar-ring: oklch(0.556 0.155 277);
--sidebar: var(--color-neutral-950);
--sidebar-foreground: var(--color-neutral-100);
--sidebar-primary: var(--color-neutral-100);
--sidebar-primary-foreground: var(--color-neutral-800);
--sidebar-accent: --alpha(var(--color-white) / 4%);
--sidebar-accent-foreground: var(--color-neutral-100);
--sidebar-border: --alpha(var(--color-white) / 6%);
--sidebar-ring: var(--color-neutral-500);
--destructive-foreground: var(--color-red-400);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-400);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
}
@layer base {
* {
@apply border-border outline-ring/50;
scrollbar-width: thin;
scrollbar-color: oklch(1 0 0 / 18%) transparent;
scrollbar-color: color-mix(in srgb, var(--foreground) 20%, transparent) transparent;
}
*::-webkit-scrollbar {
width: 8px;
@@ -132,11 +159,11 @@
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: oklch(1 0 0 / 18%);
background-color: color-mix(in srgb, var(--foreground) 20%, transparent);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: oklch(1 0 0 / 28%);
background-color: color-mix(in srgb, var(--foreground) 32%, transparent);
}
body {
@apply bg-background text-foreground;
+28 -17
View File
@@ -1,19 +1,14 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono, Inter } from "next/font/google";
import { Geist_Mono, Inter } from "next/font/google";
import "./globals.css";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "@/components/theme-provider";
import { I18nProvider } from "@/components/i18n-provider";
const inter = Inter({subsets:['latin'],variable:'--font-sans'});
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
// COSS font-variable contract: --font-sans, --font-heading, --font-mono.
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
const interHeading = Inter({ subsets: ["latin"], variable: "--font-heading" });
const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" });
export const metadata: Metadata = {
title: "temetro — AI assistant for clinicians",
@@ -29,16 +24,32 @@ export default function RootLayout({
return (
<html
lang="en"
className={cn("dark", "h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", inter.variable)}
suppressHydrationWarning
className={cn(
"h-full",
"antialiased",
inter.variable,
interHeading.variable,
geistMono.variable,
"font-sans"
)}
>
{/* suppressHydrationWarning: browser extensions (e.g. ColorZilla's
cz-shortcut-listen) mutate <body> before hydration. Only ignores
attribute diffs on <body> itself, not its children. */}
{/* suppressHydrationWarning: next-themes sets the theme class on <html>
before hydration, and browser extensions (e.g. ColorZilla's
cz-shortcut-listen) mutate <body>. Only ignores attribute diffs on
those elements, not their children. */}
<body
className="h-dvh overflow-hidden flex flex-col"
suppressHydrationWarning
>
{children}
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<I18nProvider>{children}</I18nProvider>
</ThemeProvider>
</body>
</html>
);
+2 -1
View File
@@ -22,6 +22,7 @@
"hooks": "@/hooks"
},
"registries": {
"@blocks-so": "https://blocks.so/r/{name}.json"
"@blocks-so": "https://blocks.so/r/{name}.json",
"@coss": "https://coss.com/ui/r/{name}.json"
}
}
@@ -2,10 +2,10 @@
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
PreviewCard as HoverCard,
PreviewCardPopup as HoverCardContent,
PreviewCardTrigger as HoverCardTrigger,
} from "@/components/ui/preview-card";
import { cn } from "@/lib/utils";
import type { FileUIPart, SourceDocumentUIPart } from "ai";
import {
@@ -2,9 +2,9 @@
import { Button } from "@/components/ui/button";
import {
ButtonGroup,
ButtonGroupText,
} from "@/components/ui/button-group";
Group as ButtonGroup,
GroupText as ButtonGroupText,
} from "@/components/ui/group";
import { cn } from "@/lib/utils";
import type { Experimental_SpeechResult as SpeechResult } from "ai";
import {
+4 -4
View File
@@ -2,10 +2,10 @@
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
PreviewCard as HoverCard,
PreviewCardPopup as HoverCardContent,
PreviewCardTrigger as HoverCardTrigger,
} from "@/components/ui/preview-card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
import type { LanguageModelUsage } from "ai";
@@ -8,10 +8,10 @@ import {
CarouselItem,
} from "@/components/ui/carousel";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
PreviewCard as HoverCard,
PreviewCardPopup as HoverCardContent,
PreviewCardTrigger as HoverCardTrigger,
} from "@/components/ui/preview-card";
import { cn } from "@/lib/utils";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import type { ComponentProps } from "react";
+9 -12
View File
@@ -1,13 +1,10 @@
"use client";
import { Button } from "@/components/ui/button";
import {
ButtonGroup,
ButtonGroupText,
} from "@/components/ui/button-group";
import { Group, GroupText } from "@/components/ui/group";
import {
Tooltip,
TooltipContent,
TooltipPopup,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
@@ -101,10 +98,10 @@ export const MessageAction = ({
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{button}</TooltipTrigger>
<TooltipContent>
<TooltipTrigger render={button} />
<TooltipPopup>
<p>{tooltip}</p>
</TooltipContent>
</TooltipPopup>
</Tooltip>
</TooltipProvider>
);
@@ -227,7 +224,7 @@ export const MessageBranchContent = ({
));
};
export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
export type MessageBranchSelectorProps = ComponentProps<typeof Group>;
export const MessageBranchSelector = ({
className,
@@ -241,7 +238,7 @@ export const MessageBranchSelector = ({
}
return (
<ButtonGroup
<Group
className={cn(
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
className
@@ -307,7 +304,7 @@ export const MessageBranchPage = ({
const { currentBranch, totalBranches } = useMessageBranch();
return (
<ButtonGroupText
<GroupText
className={cn(
"border-none bg-transparent text-muted-foreground shadow-none",
className
@@ -315,7 +312,7 @@ export const MessageBranchPage = ({
{...props}
>
{currentBranch + 1} of {totalBranches}
</ButtonGroupText>
</GroupText>
);
};
@@ -2,13 +2,13 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Menu as DropdownMenu,
MenuPopup as DropdownMenuContent,
MenuItem as DropdownMenuItem,
MenuGroupLabel as DropdownMenuLabel,
MenuSeparator as DropdownMenuSeparator,
MenuTrigger as DropdownMenuTrigger,
} from "@/components/ui/menu";
import { cn } from "@/lib/utils";
import {
ChevronDownIcon,
@@ -10,20 +10,20 @@ import {
CommandSeparator,
} from "@/components/ui/command";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Menu as DropdownMenu,
MenuPopup as DropdownMenuContent,
MenuItem as DropdownMenuItem,
MenuTrigger as DropdownMenuTrigger,
} from "@/components/ui/menu";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
PreviewCard as HoverCard,
PreviewCardPopup as HoverCardContent,
PreviewCardTrigger as HoverCardTrigger,
} from "@/components/ui/preview-card";
import { Button as InputGroupButton } from "@/components/ui/button";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/components/ui/input-group";
import {
+1 -1
View File
@@ -1,9 +1,9 @@
"use client";
import { Button as InputGroupButton } from "@/components/ui/button";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
+16 -16
View File
@@ -25,12 +25,12 @@ import {
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Menu,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuTrigger,
} from "@/components/ui/menu";
import { cn } from "@/lib/utils";
type ChatInputProps = {
@@ -101,8 +101,8 @@ function SelectPill({
const selected = options.find((option) => option.value === value);
return (
<DropdownMenu>
<DropdownMenuTrigger
<Menu>
<MenuTrigger
render={
<button aria-label={ariaLabel} className={triggerClassName} type="button" />
}
@@ -113,17 +113,17 @@ function SelectPill({
) : null}
<span className="truncate">{selected?.label}</span>
<ChevronDown className={chevronClassName} />
</DropdownMenuTrigger>
<DropdownMenuContent align={align}>
<DropdownMenuRadioGroup onValueChange={onValueChange} value={value}>
</MenuTrigger>
<MenuPopup align={align}>
<MenuRadioGroup onValueChange={onValueChange} value={value}>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
<MenuRadioItem key={option.value} value={option.value}>
{option.label}
</DropdownMenuRadioItem>
</MenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</MenuRadioGroup>
</MenuPopup>
</Menu>
);
}
+9 -4
View File
@@ -62,6 +62,11 @@ const sexLabel: Record<Patient["sex"], string> = { F: "Female", M: "Male" };
const rowCard =
"w-80 shrink-0 cursor-pointer text-left outline-none transition hover:ring-foreground/20 focus-visible:ring-2 focus-visible:ring-ring";
// COSS Card has no `size` variant; recreate the old compact ("sm") density by
// tightening the inner section padding from p-6 → p-4 via data-slot selectors.
const compactCard =
"[&_[data-slot=card-header]]:p-4 [&_[data-slot=card-panel]]:px-4 [&_[data-slot=card-panel]]:pb-4";
function SectionLabel({ children }: { children: ReactNode }) {
return (
<p className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
@@ -171,7 +176,7 @@ function ExpandableCard({
<Dialog>
<DialogTrigger
nativeButton={false}
render={<Card className={rowCard} size="sm" />}
render={<Card className={cn(rowCard, compactCard)} />}
>
{children}
</DialogTrigger>
@@ -529,7 +534,7 @@ function VisitsCard({ patient }: { patient: Patient }) {
function LoadingCards() {
return (
<>
<Card className="w-80 shrink-0" size="sm">
<Card className={cn("w-80 shrink-0", compactCard)}>
<CardHeader>
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-full" />
@@ -546,7 +551,7 @@ function LoadingCards() {
</CardContent>
</Card>
{[0, 1, 2, 3, 4, 5].map((card) => (
<Card className="w-80 shrink-0" key={card} size="sm">
<Card className={cn("w-80 shrink-0", compactCard)} key={card}>
<CardHeader>
<Skeleton className="h-4 w-28" />
</CardHeader>
@@ -574,7 +579,7 @@ export function PatientResult({
if (status === "not-found") {
return (
<Card size="sm">
<Card className={compactCard}>
<CardContent>
<p className="text-muted-foreground">
No patient found for file #{fileNumber}.
+10
View File
@@ -0,0 +1,10 @@
"use client";
import type * as React from "react";
import { I18nextProvider } from "react-i18next";
import i18n from "@/lib/i18n/config";
export function I18nProvider({ children }: { children: React.ReactNode }) {
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}
+19 -21
View File
@@ -3,6 +3,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { type FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
@@ -12,12 +13,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
@@ -26,6 +22,7 @@ export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
const { t } = useTranslation();
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -45,10 +42,7 @@ export function LoginForm({
});
if (err) {
setError(
err.message ??
"Could not sign in. Check your email and password and try again."
);
setError(err.message ?? t("auth.login.error"));
setSubmitting(false);
return;
}
@@ -59,24 +53,26 @@ export function LoginForm({
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle>Welcome back</CardTitle>
<CardDescription>Sign in to your clinician account</CardDescription>
<CardTitle>{t("auth.login.title")}</CardTitle>
<CardDescription>{t("auth.login.subtitle")}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit}>
<FieldGroup>
<div className="flex flex-col gap-6">
{error && (
<p className="rounded-2xl bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</p>
)}
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<FieldLabel htmlFor="email">
{t("auth.login.emailLabel")}
</FieldLabel>
<Input
autoComplete="email"
id="email"
onChange={(e) => setEmail(e.target.value)}
placeholder="you@clinic.org"
placeholder={t("auth.login.emailPlaceholder")}
required
type="email"
value={email}
@@ -84,12 +80,14 @@ export function LoginForm({
</Field>
<Field>
<div className="flex items-center">
<FieldLabel htmlFor="password">Password</FieldLabel>
<FieldLabel htmlFor="password">
{t("auth.login.passwordLabel")}
</FieldLabel>
<Link
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
href="/forgot-password"
>
Forgot your password?
{t("auth.login.forgotPassword")}
</Link>
</div>
<Input
@@ -103,14 +101,14 @@ export function LoginForm({
</Field>
<Field>
<Button disabled={submitting} type="submit">
{submitting ? "Signing in" : "Sign in"}
{submitting ? t("auth.login.submitting") : t("auth.login.submit")}
</Button>
<FieldDescription className="text-center">
Don&apos;t have an account?{" "}
<Link href="/signup">Sign up</Link>
{t("auth.login.noAccount")}{" "}
<Link href="/signup">{t("auth.login.signUpLink")}</Link>
</FieldDescription>
</Field>
</FieldGroup>
</div>
</form>
</CardContent>
</Card>
@@ -59,7 +59,7 @@ export function ProfilePanel() {
<div className="flex items-end gap-4">
<div className="space-y-1.5">
<FieldLabel>Avatar</FieldLabel>
<Avatar className="size-10 rounded-xl" size="lg">
<Avatar className="size-10 rounded-xl">
<AvatarFallback className="rounded-xl bg-muted text-sm font-medium">
K
</AvatarFallback>
+27 -22
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import {
@@ -12,14 +13,14 @@ import { CareTeamPanel } from "@/components/settings/settings-care-team";
import { ProfilePanel } from "@/components/settings/settings-preferences";
const TABS = [
"Profile",
"Records",
"Signing",
"Care team",
"Developers",
{ id: "profile", labelKey: "settings.tabs.profile" },
{ id: "records", labelKey: "settings.tabs.records" },
{ id: "signing", labelKey: "settings.tabs.signing" },
{ id: "careTeam", labelKey: "settings.tabs.careTeam" },
{ id: "developers", labelKey: "settings.tabs.developers" },
] as const;
type Tab = (typeof TABS)[number];
type Tab = (typeof TABS)[number]["id"];
function PlaceholderPanel({
title,
@@ -28,55 +29,59 @@ function PlaceholderPanel({
title: string;
description: string;
}) {
const { t } = useTranslation();
return (
<SettingsSection description={description} title={title}>
<SettingsCard className="flex items-center justify-center p-12">
<p className="text-sm text-muted-foreground">Nothing here yet.</p>
<p className="text-sm text-muted-foreground">{t("settings.empty")}</p>
</SettingsCard>
</SettingsSection>
);
}
export function SettingsView() {
const [tab, setTab] = useState<Tab>("Profile");
const { t } = useTranslation();
const [tab, setTab] = useState<Tab>("profile");
return (
<div className="mx-auto w-full max-w-3xl px-6 py-10">
<div className="flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-2xl font-semibold tracking-tight">{tab}</h1>
<h1 className="text-2xl font-semibold tracking-tight">
{t(`settings.tabs.${tab}`)}
</h1>
<nav className="flex flex-wrap items-center gap-1">
{TABS.map((item) => (
<button
className={cn(
"rounded-lg px-3 py-1.5 text-sm transition-colors",
tab === item
tab === item.id
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
key={item}
onClick={() => setTab(item)}
key={item.id}
onClick={() => setTab(item.id)}
type="button"
>
{item}
{t(item.labelKey)}
</button>
))}
</nav>
</div>
<div className="mt-10 space-y-12">
{tab === "Profile" && <ProfilePanel />}
{tab === "Records" && (
{tab === "profile" && <ProfilePanel />}
{tab === "records" && (
<PlaceholderPanel
description="How patient records are sourced, stored, and displayed"
title="Records"
description={t("settings.records.description")}
title={t("settings.tabs.records")}
/>
)}
{tab === "Signing" && <SigningPanel />}
{tab === "Care team" && <CareTeamPanel />}
{tab === "Developers" && (
{tab === "signing" && <SigningPanel />}
{tab === "careTeam" && <CareTeamPanel />}
{tab === "developers" && (
<PlaceholderPanel
description="Access tokens for the temetro API"
title="Developers"
description={t("settings.developers.description")}
title={t("settings.tabs.developers")}
/>
)}
</div>
+23 -21
View File
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { Plus, Settings, Users } from "lucide-react";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import type { Route } from "./nav-main";
import DashboardNavigation from "@/components/sidebar-02/nav-main";
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
@@ -42,31 +43,32 @@ const sampleNotifications = [
},
];
const dashboardRoutes: Route[] = [
{
id: "new-chat",
title: "New chat",
icon: <Plus className="size-4" />,
link: "/",
},
{
id: "patients",
title: "Patients",
icon: <Users className="size-4" />,
link: "/patients",
},
{
id: "settings",
title: "Settings",
icon: <Settings className="size-4" />,
link: "/settings",
},
];
export function DashboardSidebar() {
const { state } = useSidebar();
const { t } = useTranslation();
const isCollapsed = state === "collapsed";
const dashboardRoutes: Route[] = [
{
id: "new-chat",
title: t("nav.newChat"),
icon: <Plus className="size-4" />,
link: "/",
},
{
id: "patients",
title: t("nav.patients"),
icon: <Users className="size-4" />,
link: "/patients",
},
{
id: "settings",
title: t("nav.settings"),
icon: <Settings className="size-4" />,
link: "/settings",
},
];
return (
<Sidebar variant="inset" collapsible="icon">
<SidebarHeader
@@ -3,14 +3,15 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Menu,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
import { BellIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
type Notification = {
id: string;
@@ -25,14 +26,15 @@ export function NotificationsPopover({
}: {
notifications: Notification[];
}) {
const { t } = useTranslation();
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon" className="rounded-full" aria-label="Open notifications" />}><BellIcon className="size-5" /></DropdownMenuTrigger>
<DropdownMenuContent side="right" className="w-80 my-6">
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
<DropdownMenuSeparator />
<Menu>
<MenuTrigger render={<Button variant="ghost" size="icon" className="rounded-full" aria-label="Open notifications" />}><BellIcon className="size-5" /></MenuTrigger>
<MenuPopup side="right" className="w-80 my-6">
<MenuGroupLabel>{t("nav.notifications")}</MenuGroupLabel>
<MenuSeparator />
{notifications.map(({ id, avatar, fallback, text, time }) => (
<DropdownMenuItem key={id} className="flex items-start gap-3">
<MenuItem key={id} className="flex items-start gap-3">
<Avatar className="size-8">
<AvatarImage src={avatar} alt="Avatar" />
<AvatarFallback>{fallback}</AvatarFallback>
@@ -41,13 +43,13 @@ export function NotificationsPopover({
<span className="text-sm font-medium">{text}</span>
<span className="text-xs text-muted-foreground">{time}</span>
</div>
</DropdownMenuItem>
</MenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-center text-sm text-muted-foreground hover:text-primary">
View all notifications
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<MenuSeparator />
<MenuItem className="justify-center text-sm text-muted-foreground hover:text-primary">
{t("nav.viewAllNotifications")}
</MenuItem>
</MenuPopup>
</Menu>
);
}
+27 -27
View File
@@ -6,14 +6,14 @@ import { useRouter } from "next/navigation";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Menu,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuShortcut,
MenuTrigger,
} from "@/components/ui/menu";
import {
SidebarMenu,
SidebarMenuButton,
@@ -66,8 +66,8 @@ export function NavUser() {
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
<Menu>
<MenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
@@ -90,14 +90,14 @@ export function NavUser() {
<ChevronsUpDown className="ml-auto size-4" />
</>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
</MenuTrigger>
<MenuPopup
align="start"
className="min-w-56"
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
sideOffset={8}
>
<DropdownMenuLabel className="flex items-center gap-2 py-2 text-foreground">
<MenuGroupLabel className="flex items-center gap-2 py-2 text-foreground">
<Avatar className="size-8">
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
@@ -107,30 +107,30 @@ export function NavUser() {
{email}
</span>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem render={<Link href="/settings" />}>
</MenuGroupLabel>
<MenuSeparator />
<MenuItem render={<Link href="/settings" />}>
<SettingsIcon />
Settings
</DropdownMenuItem>
<DropdownMenuItem
</MenuItem>
<MenuItem
render={<a href={REPO_URL} rel="noreferrer" target="_blank" />}
>
<GitHubIcon className="size-4" />
Docs &amp; GitHub
</DropdownMenuItem>
<DropdownMenuItem>
</MenuItem>
<MenuItem>
<Sun />
Theme
<DropdownMenuShortcut>Dark</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={signOut} variant="destructive">
<MenuShortcut>Dark</MenuShortcut>
</MenuItem>
<MenuSeparator />
<MenuItem onClick={signOut} variant="destructive">
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</MenuItem>
</MenuPopup>
</Menu>
</SidebarMenuItem>
</SidebarMenu>
);
@@ -4,13 +4,13 @@ import { Building2, ChevronsUpDown, Plus } from "lucide-react";
import { useRouter } from "next/navigation";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Menu,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
import {
SidebarMenu,
SidebarMenuButton,
@@ -38,8 +38,8 @@ export function OrgSwitcher() {
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
<Menu>
<MenuTrigger
render={
<SidebarMenuButton
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
@@ -62,18 +62,18 @@ export function OrgSwitcher() {
<ChevronsUpDown className="ml-auto size-4" />
</>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
</MenuTrigger>
<MenuPopup
align="start"
className="min-w-56 rounded-lg"
side={isMobile ? "bottom" : isCollapsed ? "right" : "bottom"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
<MenuGroupLabel className="text-xs text-muted-foreground">
Clinics
</DropdownMenuLabel>
</MenuGroupLabel>
{(orgs ?? []).map((org) => (
<DropdownMenuItem
<MenuItem
className="gap-2 p-2"
key={org.id}
onClick={() => setActive(org.id)}
@@ -82,10 +82,10 @@ export function OrgSwitcher() {
<Building2 className="size-4 shrink-0" />
</div>
<span className="truncate">{org.name}</span>
</DropdownMenuItem>
</MenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
<MenuSeparator />
<MenuItem
className="gap-2 p-2"
onClick={() => router.push("/onboarding")}
>
@@ -95,9 +95,9 @@ export function OrgSwitcher() {
<div className="font-medium text-muted-foreground">
Create clinic
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</MenuItem>
</MenuPopup>
</Menu>
</SidebarMenuItem>
</SidebarMenu>
);
+28 -24
View File
@@ -3,6 +3,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { type FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
@@ -12,12 +13,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
@@ -28,6 +24,7 @@ export function SignupForm({
className,
...props
}: React.ComponentProps<"div">) {
const { t } = useTranslation();
const router = useRouter();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
@@ -42,11 +39,11 @@ export function SignupForm({
setError(null);
if (password.length < MIN_PASSWORD) {
setError(`Password must be at least ${MIN_PASSWORD} characters.`);
setError(t("auth.signup.passwordTooShort", { count: MIN_PASSWORD }));
return;
}
if (password !== confirm) {
setError("Passwords do not match.");
setError(t("auth.signup.passwordMismatch"));
return;
}
@@ -59,7 +56,7 @@ export function SignupForm({
});
if (err) {
setError(err.message ?? "Could not create your account.");
setError(err.message ?? t("auth.signup.error"));
setSubmitting(false);
return;
}
@@ -72,38 +69,40 @@ export function SignupForm({
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Create your account</CardTitle>
<CardDescription>
Start using temetro in your clinic
</CardDescription>
<CardTitle className="text-xl">{t("auth.signup.title")}</CardTitle>
<CardDescription>{t("auth.signup.subtitle")}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit}>
<FieldGroup>
<div className="flex flex-col gap-6">
{error && (
<p className="rounded-2xl bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</p>
)}
<Field>
<FieldLabel htmlFor="name">Full name</FieldLabel>
<FieldLabel htmlFor="name">
{t("auth.signup.nameLabel")}
</FieldLabel>
<Input
autoComplete="name"
id="name"
onChange={(e) => setName(e.target.value)}
placeholder="Dr. Jane Okafor"
placeholder={t("auth.signup.namePlaceholder")}
required
type="text"
value={name}
/>
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<FieldLabel htmlFor="email">
{t("auth.signup.emailLabel")}
</FieldLabel>
<Input
autoComplete="email"
id="email"
onChange={(e) => setEmail(e.target.value)}
placeholder="you@clinic.org"
placeholder={t("auth.signup.emailPlaceholder")}
required
type="email"
value={email}
@@ -112,7 +111,9 @@ export function SignupForm({
<Field>
<Field className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<FieldLabel htmlFor="password">
{t("auth.signup.passwordLabel")}
</FieldLabel>
<Input
autoComplete="new-password"
id="password"
@@ -124,7 +125,7 @@ export function SignupForm({
</Field>
<Field>
<FieldLabel htmlFor="confirm-password">
Confirm password
{t("auth.signup.confirmPasswordLabel")}
</FieldLabel>
<Input
autoComplete="new-password"
@@ -137,18 +138,21 @@ export function SignupForm({
</Field>
</Field>
<FieldDescription>
Must be at least {MIN_PASSWORD} characters long.
{t("auth.signup.passwordHint", { count: MIN_PASSWORD })}
</FieldDescription>
</Field>
<Field>
<Button disabled={submitting} type="submit">
{submitting ? "Creating account…" : "Create account"}
{submitting
? t("auth.signup.submitting")
: t("auth.signup.submit")}
</Button>
<FieldDescription className="text-center">
Already have an account? <Link href="/login">Sign in</Link>
{t("auth.signup.haveAccount")}{" "}
<Link href="/login">{t("auth.signup.signInLink")}</Link>
</FieldDescription>
</Field>
</FieldGroup>
</div>
</form>
</CardContent>
</Card>
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type * as React from "react";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+32 -39
View File
@@ -1,75 +1,68 @@
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
"use client";
import { cn } from "@/lib/utils"
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
import { ChevronDownIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
className={cn(
"flex w-full flex-col overflow-hidden rounded-2xl border",
className
)}
{...props}
/>
)
export function Accordion(
props: AccordionPrimitive.Root.Props,
): React.ReactElement {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
export function AccordionItem({
className,
...props
}: AccordionPrimitive.Item.Props): React.ReactElement {
return (
<AccordionPrimitive.Item
className={cn("border-b last:border-b-0", className)}
data-slot="accordion-item"
className={cn("not-last:border-b data-open:bg-muted/50", className)}
{...props}
/>
)
);
}
function AccordionTrigger({
export function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
}: AccordionPrimitive.Trigger.Props): React.ReactElement {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"group/accordion-trigger relative flex flex-1 items-start justify-between gap-6 border border-transparent p-4 text-left text-sm font-medium transition-all outline-none hover:underline aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
className
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180",
className,
)}
data-slot="accordion-trigger"
{...props}
>
{children}
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
<ChevronDownIcon
className="pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-80 transition-transform duration-200 ease-in-out"
data-slot="accordion-indicator"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
);
}
function AccordionContent({
export function AccordionPanel({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
}: AccordionPrimitive.Panel.Props): React.ReactElement {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className="overflow-hidden px-4 text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
className="h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0"
data-slot="accordion-panel"
{...props}
>
<div
className={cn(
"h-(--accordion-panel-height) pt-0 pb-4 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
>
{children}
</div>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Panel>
)
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { AccordionPrimitive, AccordionPanel as AccordionContent };
+50 -42
View File
@@ -1,76 +1,84 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-2xl border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
"relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
}
)
variants: {
variant: {
default:
"bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
error:
"border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
info: "border-info/32 bg-info/4 [&>svg]:text-info",
success: "border-success/32 bg-success/4 [&>svg]:text-success",
warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
},
},
},
);
function Alert({
export function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
}: React.ComponentProps<"div"> &
VariantProps<typeof alertVariants>): React.ReactElement {
return (
<div
className={cn(alertVariants({ variant }), className)}
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
function AlertDescription({
export function AlertTitle({
className,
...props
}: React.ComponentProps<"div">) {
}: React.ComponentProps<"div">): React.ReactElement {
return (
<div
className={cn("font-medium [svg~&]:col-start-2", className)}
data-slot="alert-title"
{...props}
/>
);
}
export function AlertDescription({
className,
...props
}: React.ComponentProps<"div">): React.ReactElement {
return (
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
"flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2",
className,
)}
data-slot="alert-description"
{...props}
/>
)
);
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
export function AlertAction({
className,
...props
}: React.ComponentProps<"div">): React.ReactElement {
return (
<div
className={cn(
"flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3",
className,
)}
data-slot="alert-action"
className={cn("absolute top-2.5 right-3", className)}
{...props}
/>
)
);
}
export { Alert, AlertTitle, AlertDescription, AlertAction }
+316
View File
@@ -0,0 +1,316 @@
"use client";
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
import { ChevronsUpDownIcon, XIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
export const Autocomplete: typeof AutocompletePrimitive.Root =
AutocompletePrimitive.Root;
export function AutocompleteInput({
className,
showTrigger = false,
showClear = false,
startAddon,
size,
triggerProps,
clearProps,
...props
}: Omit<AutocompletePrimitive.Input.Props, "size"> & {
showTrigger?: boolean;
showClear?: boolean;
startAddon?: React.ReactNode;
size?: "sm" | "default" | "lg" | number;
ref?: React.Ref<HTMLInputElement>;
triggerProps?: AutocompletePrimitive.Trigger.Props;
clearProps?: AutocompletePrimitive.Clear.Props;
}): React.ReactElement {
const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
return (
<AutocompletePrimitive.InputGroup
className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64"
data-slot="autocomplete-input-group"
>
{startAddon && (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5"
data-slot="autocomplete-start-addon"
>
{startAddon}
</div>
)}
<AutocompletePrimitive.Input
className={cn(
startAddon &&
"data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]",
sizeValue === "sm"
? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
: "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
className,
)}
data-slot="autocomplete-input"
render={<Input nativeInput size={sizeValue} />}
{...props}
/>
{showTrigger && (
<AutocompleteTrigger
className={cn(
"absolute top-1/2 inline-flex size-8 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
{...triggerProps}
>
<AutocompletePrimitive.Icon data-slot="autocomplete-icon">
<ChevronsUpDownIcon />
</AutocompletePrimitive.Icon>
</AutocompleteTrigger>
)}
{showClear && (
<AutocompleteClear
className={cn(
"absolute top-1/2 inline-flex size-8 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
{...clearProps}
>
<XIcon />
</AutocompleteClear>
)}
</AutocompletePrimitive.InputGroup>
);
}
export function AutocompletePopup({
className,
children,
side = "bottom",
sideOffset = 4,
alignOffset,
align = "start",
anchor,
portalProps,
...props
}: AutocompletePrimitive.Popup.Props & {
align?: AutocompletePrimitive.Positioner.Props["align"];
sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"];
alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"];
side?: AutocompletePrimitive.Positioner.Props["side"];
anchor?: AutocompletePrimitive.Positioner.Props["anchor"];
portalProps?: AutocompletePrimitive.Portal.Props;
}): React.ReactElement {
return (
<AutocompletePrimitive.Portal {...portalProps}>
<AutocompletePrimitive.Positioner
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="z-50 select-none"
data-slot="autocomplete-positioner"
side={side}
sideOffset={sideOffset}
>
<span
className={cn(
"relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
>
<AutocompletePrimitive.Popup
className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground"
data-slot="autocomplete-popup"
{...props}
>
{children}
</AutocompletePrimitive.Popup>
</span>
</AutocompletePrimitive.Positioner>
</AutocompletePrimitive.Portal>
);
}
export function AutocompleteItem({
className,
children,
...props
}: AutocompletePrimitive.Item.Props): React.ReactElement {
return (
<AutocompletePrimitive.Item
className={cn(
"flex min-h-8 cursor-default select-none items-center rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm",
className,
)}
data-slot="autocomplete-item"
{...props}
>
{children}
</AutocompletePrimitive.Item>
);
}
export function AutocompleteSeparator({
className,
...props
}: AutocompletePrimitive.Separator.Props): React.ReactElement {
return (
<AutocompletePrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
data-slot="autocomplete-separator"
{...props}
/>
);
}
export function AutocompleteGroup({
className,
...props
}: AutocompletePrimitive.Group.Props): React.ReactElement {
return (
<AutocompletePrimitive.Group
className={cn("[[role=group]+&]:mt-1.5", className)}
data-slot="autocomplete-group"
{...props}
/>
);
}
export function AutocompleteGroupLabel({
className,
...props
}: AutocompletePrimitive.GroupLabel.Props): React.ReactElement {
return (
<AutocompletePrimitive.GroupLabel
className={cn(
"px-2 py-1.5 font-medium text-muted-foreground text-xs",
className,
)}
data-slot="autocomplete-group-label"
{...props}
/>
);
}
export function AutocompleteEmpty({
className,
...props
}: AutocompletePrimitive.Empty.Props): React.ReactElement {
return (
<AutocompletePrimitive.Empty
className={cn(
"not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
className,
)}
data-slot="autocomplete-empty"
{...props}
/>
);
}
export function AutocompleteRow({
className,
...props
}: AutocompletePrimitive.Row.Props): React.ReactElement {
return (
<AutocompletePrimitive.Row
className={className}
data-slot="autocomplete-row"
{...props}
/>
);
}
export function AutocompleteValue({
...props
}: AutocompletePrimitive.Value.Props): React.ReactElement {
return (
<AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
);
}
export function AutocompleteList({
className,
...props
}: AutocompletePrimitive.List.Props): React.ReactElement {
return (
<ScrollArea scrollbarGutter scrollFade>
<AutocompletePrimitive.List
className={cn(
"not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3",
className,
)}
data-slot="autocomplete-list"
{...props}
/>
</ScrollArea>
);
}
export function AutocompleteClear({
className,
...props
}: AutocompletePrimitive.Clear.Props): React.ReactElement {
return (
<AutocompletePrimitive.Clear
className={cn(
"absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="autocomplete-clear"
{...props}
>
<XIcon />
</AutocompletePrimitive.Clear>
);
}
export function AutocompleteStatus({
className,
...props
}: AutocompletePrimitive.Status.Props): React.ReactElement {
return (
<AutocompletePrimitive.Status
className={cn(
"px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
className,
)}
data-slot="autocomplete-status"
{...props}
/>
);
}
export function AutocompleteCollection({
...props
}: AutocompletePrimitive.Collection.Props): React.ReactElement {
return (
<AutocompletePrimitive.Collection
data-slot="autocomplete-collection"
{...props}
/>
);
}
export function AutocompleteTrigger({
className,
children,
...props
}: AutocompletePrimitive.Trigger.Props): React.ReactElement {
return (
<AutocompletePrimitive.Trigger
className={className}
data-slot="autocomplete-trigger"
{...props}
>
{children}
</AutocompletePrimitive.Trigger>
);
}
export const useAutocompleteFilter: typeof AutocompletePrimitive.useFilter =
AutocompletePrimitive.useFilter;
export { AutocompletePrimitive };
+24 -81
View File
@@ -1,109 +1,52 @@
"use client"
"use client";
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
function Avatar({
export function Avatar({
className,
size = "default",
...props
}: AvatarPrimitive.Root.Props & {
size?: "default" | "sm" | "lg"
}) {
}: AvatarPrimitive.Root.Props): React.ReactElement {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
"inline-flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-background align-middle font-medium text-xs",
className,
)}
data-slot="avatar"
{...props}
/>
)
);
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
export function AvatarImage({
className,
...props
}: AvatarPrimitive.Image.Props): React.ReactElement {
return (
<AvatarPrimitive.Image
className={cn("size-full object-cover", className)}
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
);
}
function AvatarFallback({
export function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
}: AvatarPrimitive.Fallback.Props): React.ReactElement {
return (
<AvatarPrimitive.Fallback
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted",
className,
)}
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
);
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}
export { AvatarPrimitive };
+54 -42
View File
@@ -1,52 +1,64 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
"use client";
import { cn } from "@/lib/utils"
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import type React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
export const badgeVariants = cva(
"relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button&,a&]:cursor-pointer [button&,a&]:pointer-coarse:after:absolute [button&,a&]:pointer-coarse:after:size-full [button&,a&]:pointer-coarse:after:min-h-11 [button&,a&]:pointer-coarse:after:min-w-11",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
size: "default",
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(
{
className: cn(badgeVariants({ variant }), className),
variants: {
size: {
default:
"h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs",
lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm",
sm: "h-5 min-w-5 rounded-[.25rem] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
},
variant: {
default:
"bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",
destructive:
"bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
error:
"bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
info: "bg-info/8 text-info-foreground dark:bg-info/16",
outline:
"border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48",
secondary:
"bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
success: "bg-success/8 text-success-foreground dark:bg-success/16",
warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
},
props
),
render,
state: {
slot: "badge",
variant,
},
})
},
);
export interface BadgeProps extends useRender.ComponentProps<"span"> {
variant?: VariantProps<typeof badgeVariants>["variant"];
size?: VariantProps<typeof badgeVariants>["size"];
}
export { Badge, badgeVariants }
export function Badge({
className,
variant,
size,
render,
...props
}: BadgeProps): React.ReactElement {
const defaultProps = {
className: cn(badgeVariants({ className, size, variant })),
"data-slot": "badge",
};
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(defaultProps, props),
render,
});
}
-87
View File
@@ -1,87 +0,0 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[>[data-variant=outline]]:*:data-[slot=input-group]:border-border has-[>[data-variant=outline]]:*:data-[slot=select-trigger]:border-border has-[>[data-variant=outline]]:[&>[data-slot=input-group]:has(:focus-visible)]:border-ring has-[>[data-variant=outline]]:[&>[data-slot=select-trigger]:focus-visible]:border-ring has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-4xl [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[>[data-variant=outline]]:[&>input]:border-border has-[>[data-variant=outline]]:[&>input:focus-visible]:border-ring",
{
variants: {
orientation: {
horizontal:
"*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-4xl! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
vertical:
"flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-4xl! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
render,
...props
}: useRender.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"flex items-center gap-2 rounded-4xl border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
),
},
props
),
render,
state: {
slot: "button-group-text",
},
})
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
+87 -47
View File
@@ -1,56 +1,96 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
"use client";
import { cn } from "@/lib/utils"
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
import { Spinner } from "@/components/ui/spinner";
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
export const buttonVariants = cva(
"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-loading:select-none data-loading:text-transparent sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-9",
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: "default",
},
}
)
variants: {
size: {
default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
icon: "size-9 sm:size-8",
"icon-lg": "size-10 sm:size-9",
"icon-sm": "size-8 sm:size-7",
"icon-xl":
"size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
"icon-xs":
"size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",
xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5",
},
variant: {
default:
"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
destructive:
"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
"destructive-outline":
"border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none",
ghost:
"border-transparent text-foreground hover:bg-accent data-pressed:bg-accent *:data-[slot=button-loading-indicator]:text-foreground",
link: "border-transparent text-foreground underline-offset-4 hover:underline data-pressed:underline *:data-[slot=button-loading-indicator]:text-foreground",
outline:
"border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90 *:data-[slot=button-loading-indicator]:text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80",
},
},
},
);
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
export interface ButtonProps extends useRender.ComponentProps<"button"> {
variant?: VariantProps<typeof buttonVariants>["variant"];
size?: VariantProps<typeof buttonVariants>["size"];
loading?: boolean;
}
export { Button, buttonVariants }
export function Button({
className,
variant,
size,
render,
children,
loading = false,
disabled: disabledProp,
...props
}: ButtonProps): React.ReactElement {
const isDisabled: boolean = Boolean(loading || disabledProp);
const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
render ? undefined : "button";
const defaultProps = {
children: (
<>
{children}
{loading && (
<Spinner
className="pointer-events-none absolute"
data-slot="button-loading-indicator"
/>
)}
</>
),
className: cn(buttonVariants({ className, size, variant })),
"aria-disabled": loading || undefined,
"data-loading": loading ? "" : undefined,
"data-slot": "button",
disabled: isDisabled,
type: typeValue,
};
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(defaultProps, props),
render,
});
}
+234 -81
View File
@@ -1,100 +1,253 @@
import * as React from "react"
"use client";
import { cn } from "@/lib/utils"
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import type React from "react";
import { cn } from "@/lib/utils";
function Card({
export function Card({
className,
size = "default",
render,
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-6 overflow-hidden rounded-4xl bg-card py-6 text-sm text-card-foreground shadow-md ring-1 ring-foreground/5 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 dark:ring-foreground/10 *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl",
className
)}
{...props}
/>
)
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
),
"data-slot": "card",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-4xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
className
)}
{...props}
/>
)
export function CardFrame({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 [--clip-bottom:-1rem] [--clip-top:-1rem] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:bg-muted/72 before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=table-container]:overflow-hidden *:data-[slot=card]:-m-px *:data-[slot=table-container]:-m-px *:data-[slot=table-container]:w-[calc(100%+2px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-last:data-[slot=card]:rounded-b-xl *:data-[slot=card]:bg-clip-padding *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:[clip-path:inset(var(--clip-top)_1px_var(--clip-bottom)_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:last:[--clip-bottom:1px] *:data-[slot=card]:first:[--clip-top:1px]",
className,
),
"data-slot": "card-frame",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("font-heading text-base font-medium", className)}
{...props}
/>
)
export function CardFrameHeader({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"relative flex grid auto-rows-min grid-rows-[auto_auto] flex-col items-start gap-x-4 px-6 py-4 has-data-[slot=card-frame-action]:grid-cols-[1fr_auto]",
className,
),
"data-slot": "card-frame-header",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
export function CardFrameTitle({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn("self-center font-semibold text-sm", className),
"data-slot": "card-frame-title",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
export function CardFrameDescription({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn("self-center text-muted-foreground text-sm", className),
"data-slot": "card-frame-description",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
{...props}
/>
)
export function CardFrameAction({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"col-start-2 nth-3:row-span-2 nth-3:row-start-1 inline-flex self-center justify-self-end",
className,
),
"data-slot": "card-frame-action",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-4xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
className
)}
{...props}
/>
)
export function CardFrameFooter({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn("px-6 py-4", className),
"data-slot": "card-frame-footer",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
export function CardHeader({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
className,
),
"data-slot": "card-header",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function CardTitle({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn("font-semibold text-lg leading-none", className),
"data-slot": "card-title",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function CardDescription({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn("text-muted-foreground text-sm", className),
"data-slot": "card-description",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function CardAction({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"col-start-2 row-span-2 row-start-1 inline-flex self-start justify-self-end",
className,
),
"data-slot": "card-action",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function CardPanel({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0",
className,
),
"data-slot": "card-panel",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function CardFooter({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4",
className,
),
"data-slot": "card-footer",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export { CardPanel as CardContent };
+32 -11
View File
@@ -1,21 +1,42 @@
"use client"
"use client";
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
import type React from "react";
import { cn } from "@/lib/utils";
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
export function Collapsible({
...props
}: CollapsiblePrimitive.Root.Props): React.ReactElement {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
export function CollapsibleTrigger({
className,
...props
}: CollapsiblePrimitive.Trigger.Props): React.ReactElement {
return (
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
)
<CollapsiblePrimitive.Trigger
className={className}
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
export function CollapsiblePanel({
className,
...props
}: CollapsiblePrimitive.Panel.Props): React.ReactElement {
return (
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
)
<CollapsiblePrimitive.Panel
className={cn(
"h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 data-ending-style:h-0 data-starting-style:h-0",
className,
)}
data-slot="collapsible-panel"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
export { CollapsiblePrimitive, CollapsiblePanel as CollapsibleContent };
+211 -140
View File
@@ -1,196 +1,267 @@
"use client"
"use client";
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
import { SearchIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
InputGroup,
InputGroupAddon,
} from "@/components/ui/input-group"
import { SearchIcon, CheckIcon } from "lucide-react"
Autocomplete,
AutocompleteCollection,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteSeparator,
} from "@/components/ui/autocomplete";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
export const CommandDialog: typeof CommandDialogPrimitive.Root =
CommandDialogPrimitive.Root;
export const CommandDialogPortal: typeof CommandDialogPrimitive.Portal =
CommandDialogPrimitive.Portal;
export const CommandCreateHandle: typeof CommandDialogPrimitive.createHandle =
CommandDialogPrimitive.createHandle;
export function CommandDialogTrigger(
props: CommandDialogPrimitive.Trigger.Props,
): React.ReactElement {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex size-full flex-col overflow-hidden rounded-4xl bg-popover p-1 text-popover-foreground",
className
)}
<CommandDialogPrimitive.Trigger
data-slot="command-dialog-trigger"
{...props}
/>
)
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
export function CommandDialogBackdrop({
className,
...props
}: CommandDialogPrimitive.Backdrop.Props): React.ReactElement {
return (
<CommandDialogPrimitive.Backdrop
className={cn(
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="command-dialog-backdrop"
{...props}
/>
);
}
export function CommandDialogViewport({
className,
...props
}: CommandDialogPrimitive.Viewport.Props): React.ReactElement {
return (
<CommandDialogPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 flex flex-col items-center px-4 py-[max(--spacing(4),4vh)] sm:py-[10vh]",
className,
)}
data-slot="command-dialog-viewport"
{...props}
/>
);
}
export function CommandDialogPopup({
className,
children,
className,
showCloseButton = false,
portalProps,
...props
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
children: React.ReactNode
}) {
}: CommandDialogPrimitive.Popup.Props & {
portalProps?: CommandDialogPrimitive.Portal.Props;
}): React.ReactElement {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-4xl! p-0",
className
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-9 bg-input/50">
<CommandPrimitive.Input
data-slot="command-input"
<CommandDialogPortal {...portalProps}>
<CommandDialogBackdrop />
<CommandDialogViewport>
<CommandDialogPrimitive.Popup
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
"relative row-start-2 flex max-h-105 min-h-0 w-full min-w-0 max-w-xl -translate-y-[calc(1.25rem*var(--nested-dialogs))] scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:bg-muted/72 before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
data-slot="command-dialog-popup"
{...props}
/>
<InputGroupAddon>
<SearchIcon className="size-4 shrink-0 opacity-50" />
</InputGroupAddon>
</InputGroup>
>
{children}
</CommandDialogPrimitive.Popup>
</CommandDialogViewport>
</CommandDialogPortal>
);
}
export function Command({
autoHighlight = "always",
keepHighlight = true,
...props
}: React.ComponentProps<typeof Autocomplete>): React.ReactElement {
return (
<Autocomplete
autoHighlight={autoHighlight}
inline
keepHighlight={keepHighlight}
open
{...props}
/>
);
}
export function CommandInput({
className,
placeholder = undefined,
...props
}: React.ComponentProps<typeof AutocompleteInput>): React.ReactElement {
return (
<div className="px-2.5 py-1.5">
<AutocompleteInput
autoFocus
className={cn(
"border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0",
className,
)}
placeholder={placeholder}
size="lg"
startAddon={<SearchIcon />}
{...props}
/>
</div>
)
);
}
function CommandList({
export function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
}: React.ComponentProps<typeof AutocompleteList>): React.ReactElement {
return (
<CommandPrimitive.List
<AutocompleteList
className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
data-slot="command-list"
className={cn(
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
className
)}
{...props}
/>
)
);
}
function CommandEmpty({
export function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
}: React.ComponentProps<typeof AutocompleteEmpty>): React.ReactElement {
return (
<CommandPrimitive.Empty
<AutocompleteEmpty
className={cn("not-empty:py-6", className)}
data-slot="command-empty"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
)
);
}
function CommandGroup({
export function CommandPanel({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
}: React.ComponentProps<"div">): React.ReactElement {
return (
<CommandPrimitive.Group
<div
className={cn(
"relative -mx-px not-has-[+[data-slot=command-footer]]:-mb-px min-h-0 rounded-t-xl not-has-[+[data-slot=command-footer]]:rounded-b-2xl border border-b-0 bg-popover bg-clip-padding shadow-xs/5 [clip-path:inset(0_1px)] not-has-[+[data-slot=command-footer]]:[clip-path:inset(0_1px_1px_1px_round_0_0_calc(var(--radius-2xl)-1px)_calc(var(--radius-2xl)-1px))] before:pointer-events-none before:absolute before:inset-0 before:rounded-t-[calc(var(--radius-xl)-1px)] **:data-[slot=scroll-area-scrollbar]:mt-2",
className,
)}
{...props}
/>
);
}
export function CommandGroup({
className,
...props
}: React.ComponentProps<typeof AutocompleteGroup>): React.ReactElement {
return (
<AutocompleteGroup
className={className}
data-slot="command-group"
className={cn(
"overflow-hidden p-1.5 text-foreground **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
)
);
}
function CommandSeparator({
export function CommandGroupLabel({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
}: React.ComponentProps<typeof AutocompleteGroupLabel>): React.ReactElement {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("my-1.5 h-px bg-border/50", className)}
<AutocompleteGroupLabel
className={className}
data-slot="command-group-label"
{...props}
/>
)
);
}
function CommandItem({
className,
children,
export function CommandCollection({
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
}: React.ComponentProps<typeof AutocompleteCollection>): React.ReactElement {
return <AutocompleteCollection data-slot="command-collection" {...props} />;
}
export function CommandItem({
className,
...props
}: React.ComponentProps<typeof AutocompleteItem>): React.ReactElement {
return (
<CommandPrimitive.Item
<AutocompleteItem
className={cn("py-1.5", className)}
data-slot="command-item"
className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-2xl px-3 py-2 text-sm font-medium outline-hidden select-none in-data-[slot=dialog-content]:rounded-3xl data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
className
)}
{...props}
>
{children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
</CommandPrimitive.Item>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
className
)}
{...props}
/>
)
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
export function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof AutocompleteSeparator>): React.ReactElement {
return (
<AutocompleteSeparator
className={cn("my-2", className)}
data-slot="command-separator"
{...props}
/>
);
}
export function CommandShortcut({
className,
...props
}: React.ComponentProps<"kbd">): React.ReactElement {
return (
<kbd
className={cn(
"ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
className,
)}
data-slot="command-shortcut"
{...props}
/>
);
}
export function CommandFooter({
className,
...props
}: React.ComponentProps<"div">): React.ReactElement {
return (
<div
className={cn(
"flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] border-t px-5 py-3 text-muted-foreground text-xs",
className,
)}
data-slot="command-footer"
{...props}
/>
);
}
export { CommandDialogPrimitive };
+169 -109
View File
@@ -1,160 +1,220 @@
"use client"
"use client";
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { XIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
export const DialogCreateHandle: typeof DialogPrimitive.createHandle =
DialogPrimitive.createHandle;
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
export const Dialog: typeof DialogPrimitive.Root = DialogPrimitive.Root;
export const DialogPortal: typeof DialogPrimitive.Portal =
DialogPrimitive.Portal;
export function DialogTrigger(
props: DialogPrimitive.Trigger.Props,
): React.ReactElement {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
export function DialogClose(
props: DialogPrimitive.Close.Props,
): React.ReactElement {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
export function DialogBackdrop({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
}: DialogPrimitive.Backdrop.Props): React.ReactElement {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="dialog-backdrop"
{...props}
/>
)
);
}
function DialogContent({
export function DialogViewport({
className,
...props
}: DialogPrimitive.Viewport.Props): React.ReactElement {
return (
<DialogPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4",
className,
)}
data-slot="dialog-viewport"
{...props}
/>
);
}
export function DialogPopup({
className,
children,
showCloseButton = true,
bottomStickOnMobile = true,
closeProps,
portalProps,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
}) {
showCloseButton?: boolean;
bottomStickOnMobile?: boolean;
closeProps?: DialogPrimitive.Close.Props;
portalProps?: DialogPrimitive.Portal.Props;
}): React.ReactElement {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
<DialogPortal {...portalProps}>
<DialogBackdrop />
<DialogViewport
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-4xl bg-popover p-6 text-sm text-popover-foreground shadow-xl ring-1 ring-foreground/5 duration-100 outline-none sm:max-w-md dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
bottomStickOnMobile &&
"max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-4 right-4 bg-secondary"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
<DialogPrimitive.Popup
className={cn(
"relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg origin-center flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-var(--nested-dialogs))] shadow-lg/5 outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 sm:scale-[calc(1-0.1*var(--nested-dialogs))] sm:data-ending-style:scale-98 sm:data-starting-style:scale-98 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
bottomStickOnMobile &&
"max-sm:max-w-none max-sm:origin-bottom max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",
className,
)}
data-slot="dialog-popup"
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
aria-label="Close"
className="absolute end-2 top-2"
render={<Button size="icon" variant="ghost" />}
{...closeProps}
>
<XIcon />
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogViewport>
</DialogPortal>
)
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-1.5", className)}
{...props}
/>
)
}
function DialogFooter({
export function DialogHeader({
className,
showCloseButton = false,
children,
render,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4",
className,
),
"data-slot": "dialog-header",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
export function DialogFooter({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"div"> & {
variant?: "default" | "bare";
}): React.ReactElement {
const defaultProps = {
className: cn(
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
variant === "default" && "border-t bg-muted/72 py-4",
variant === "bare" &&
"in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pt-3 pt-4 pb-6",
className,
),
"data-slot": "dialog-footer",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
export function DialogTitle({
className,
...props
}: DialogPrimitive.Title.Props): React.ReactElement {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
"font-heading font-semibold text-xl leading-none",
className,
)}
data-slot="dialog-title"
{...props}
/>
)
);
}
function DialogDescription({
export function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
}: DialogPrimitive.Description.Props): React.ReactElement {
return (
<DialogPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
);
}
export function DialogPanel({
className,
scrollFade = true,
render,
...props
}: useRender.ComponentProps<"div"> & {
scrollFade?: boolean;
}): React.ReactElement {
const defaultProps = {
className: cn(
"p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-header])]:pt-1 in-[[data-slot=dialog-popup]:has([data-slot=dialog-footer]:not(.border-t))]:pb-1",
className,
),
"data-slot": "dialog-panel",
};
return (
<ScrollArea scrollFade={scrollFade}>
{useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
})}
</ScrollArea>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
DialogPrimitive,
DialogBackdrop as DialogOverlay,
DialogPopup as DialogContent,
};
-271
View File
@@ -1,271 +0,0 @@
"use client"
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "@/lib/utils"
import { ChevronRightIcon, CheckIcon } from "lucide-react"
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-3xl bg-popover p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
)
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<"div"> & {
inset?: boolean
}) {
// Base UI's Menu.GroupLabel requires a surrounding Menu.Group (unlike Radix's
// standalone Label). These menus use the label as a plain heading, so render a
// <div> directly to avoid the "MenuGroupContext is missing" error.
return (
<div
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-3 py-2.5 text-xs text-muted-foreground data-inset:pl-9.5",
className
)}
{...props}
/>
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-2.5 rounded-2xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-2xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
)
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn("w-auto min-w-36 rounded-3xl bg-popover p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon
/>
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1.5 my-1.5 h-px bg-border/50", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+43 -201
View File
@@ -1,238 +1,80 @@
"use client"
"use client";
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Field as FieldPrimitive } from "@base-ui/react/field";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
export function Field({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
}: FieldPrimitive.Root.Props): React.ReactElement {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
<FieldPrimitive.Root
className={cn("flex flex-col items-start gap-2", className)}
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
);
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
export function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
}: FieldPrimitive.Label.Props): React.ReactElement {
return (
<Label
data-slot="field-label"
<FieldPrimitive.Label
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:bg-input/30 has-[>[data-slot=field]]:rounded-2xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-4",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
"inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground data-disabled:opacity-64 sm:text-sm/4",
className,
)}
data-slot="field-label"
{...props}
/>
)
);
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
export function FieldItem({
className,
...props
}: FieldPrimitive.Item.Props): React.ReactElement {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
<FieldPrimitive.Item
className={cn("flex", className)}
data-slot="field-item"
{...props}
/>
)
);
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
export function FieldDescription({
className,
...props
}: FieldPrimitive.Description.Props): React.ReactElement {
return (
<p
<FieldPrimitive.Description
className={cn("text-muted-foreground text-xs", className)}
data-slot="field-description"
className={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
);
}
function FieldSeparator({
children,
export function FieldError({
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
}: FieldPrimitive.Error.Props): React.ReactElement {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
<FieldPrimitive.Error
className={cn("text-destructive-foreground text-xs", className)}
data-slot="field-error"
className={cn("text-sm font-normal text-destructive", className)}
{...props}
>
{content}
</div>
)
/>
);
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
export const FieldControl: typeof FieldPrimitive.Control =
FieldPrimitive.Control;
export const FieldValidity: typeof FieldPrimitive.Validity =
FieldPrimitive.Validity;
export { FieldPrimitive };
+92
View File
@@ -0,0 +1,92 @@
"use client";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
export const groupVariants = cva(
"flex w-fit *:focus-visible:z-1 has-[>[data-slot=group]]:gap-2 *:has-focus-visible:z-1 dark:*:[[data-slot=separator]:has(~button:hover):not(:has(~[data-slot=separator]~[data-slot]:hover)),[data-slot=separator]:has(~[data-slot][data-pressed]):not(:has(~[data-slot=separator]~[data-slot][data-pressed]))]:before:bg-input/64 dark:*:[button:hover~[data-slot=separator]:not([data-slot]:hover~[data-slot=separator]~[data-slot=separator]),[data-slot][data-pressed]~[data-slot=separator]:not([data-slot][data-pressed]~[data-slot=separator]~[data-slot=separator])]:before:bg-input/64",
{
defaultVariants: {
orientation: "horizontal",
},
variants: {
orientation: {
horizontal:
"*:pointer-coarse:after:min-w-auto *:data-slot:has-[~[data-slot]]:rounded-e-none *:data-slot:has-[~[data-slot]]:border-e-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-end-[0.5px] *:data-slot:has-[~[data-slot]]:before:rounded-e-none *:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-start-[0.5px] *:[[data-slot]~[data-slot]]:rounded-s-none *:[[data-slot]~[data-slot]]:border-s-0 *:[[data-slot]~[data-slot]]:before:rounded-s-none",
vertical:
"flex-col *:pointer-coarse:after:min-h-auto *:data-slot:has-[~[data-slot]]:rounded-b-none *:data-slot:has-[~[data-slot]]:border-b-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-bottom-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:hidden *:data-slot:has-[~[data-slot]]:before:rounded-b-none dark:*:last:before:hidden dark:*:first:before:block *:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-top-[0.5px] *:[[data-slot]~[data-slot]]:rounded-t-none *:[[data-slot]~[data-slot]]:border-t-0 *:[[data-slot]~[data-slot]]:before:rounded-t-none",
},
},
},
);
export function Group({
className,
orientation,
children,
...props
}: {
className?: string;
orientation?: VariantProps<typeof groupVariants>["orientation"];
children: React.ReactNode;
} & React.ComponentProps<"div">): React.ReactElement {
return (
<div
className={cn(groupVariants({ orientation }), className)}
data-orientation={orientation}
data-slot="group"
role="group"
{...props}
>
{children}
</div>
);
}
export function GroupText({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"relative inline-flex items-center gap-2 whitespace-nowrap rounded-lg border border-input bg-muted not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-base text-muted-foreground shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] sm:text-sm dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5 [&_svg]:shrink-0",
className,
),
"data-slot": "group-text",
};
return useRender({
defaultTagName: "div",
props: mergeProps(defaultProps, props),
render,
});
}
export function GroupSeparator({
className,
orientation = "vertical",
...props
}: {
className?: string;
} & React.ComponentProps<typeof Separator>): React.ReactElement {
return (
<Separator
className={cn(
"pointer-events-none relative z-2 bg-input before:absolute before:inset-0 has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:translate-x-px has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:bg-ring dark:before:bg-input/32 [[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+&,[data-slot=number-field]:focus-within+input+&]:bg-ring [[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+input+&]:-translate-x-px",
className,
)}
orientation={orientation}
{...props}
/>
);
}
export {
Group as ButtonGroup,
GroupText as ButtonGroupText,
GroupSeparator as ButtonGroupSeparator,
};
-51
View File
@@ -1,51 +0,0 @@
"use client"
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"
import { cn } from "@/lib/utils"
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
return (
<PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
}
function HoverCardContent({
className,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 4,
...props
}: PreviewCardPrimitive.Popup.Props &
Pick<
PreviewCardPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
<PreviewCardPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PreviewCardPrimitive.Popup
data-slot="hover-card-content"
className={cn(
"z-50 w-72 origin-(--transform-origin) rounded-3xl bg-popover p-4 text-sm text-popover-foreground shadow-lg ring-1 ring-foreground/5 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
/>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }
+68 -118
View File
@@ -1,155 +1,105 @@
"use client"
"use client";
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-9 w-full min-w-0 items-center rounded-4xl border border-transparent bg-input/50 transition-[color,box-shadow,background-color] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-3xl has-data-[align=block-start]:rounded-3xl has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-2xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
import { Input, type InputProps } from "@/components/ui/input";
import { Textarea, type TextareaProps } from "@/components/ui/textarea";
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-2 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 **:data-[slot=kbd]:rounded-3xl **:data-[slot=kbd]:bg-muted-foreground/10 **:data-[slot=kbd]:px-1.5 [&>svg:not([class*='size-'])]:size-4",
"flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
{
variants: {
align: {
"inline-start": "order-first pl-3 has-[>button]:-ml-1 has-[>kbd]:-ml-1",
"inline-end": "order-last pr-3 has-[>button]:-mr-1 has-[>kbd]:-mr-1",
"block-start":
"order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-3.5 [.border-b]:pb-3.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-3.5 [.border-t]:pt-3.5",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
variants: {
align: {
"block-end":
"order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
"block-start":
"order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
"inline-end":
"order-last pe-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
"inline-start":
"order-first ps-[calc(--spacing(3)-1px)] has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
},
},
},
);
function InputGroupAddon({
export function InputGroup({
className,
...props
}: React.ComponentProps<"div">): React.ReactElement {
return (
<div
className={cn(
"relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
className,
)}
data-slot="input-group"
role="group"
{...props}
/>
);
}
export function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
}: React.ComponentProps<"div"> &
VariantProps<typeof inputGroupAddonVariants>): React.ReactElement {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
data-align={align}
data-slot="input-group-addon"
onMouseDown={(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
const isInteractive = target.closest(
"button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
);
if (isInteractive) return;
e.preventDefault();
const parent = e.currentTarget.parentElement;
const input = parent?.querySelector<
HTMLInputElement | HTMLTextAreaElement
>("input, textarea");
if (input && !parent?.querySelector("input:focus, textarea:focus")) {
input.focus();
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
);
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 rounded-4xl text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-xl px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs": "size-6 rounded-xl p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
export function InputGroupText({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: "button" | "submit" | "reset"
}) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
}: React.ComponentProps<"span">): React.ReactElement {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
"line-clamp-1 flex items-center gap-2 whitespace-nowrap text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5",
className,
)}
{...props}
/>
)
);
}
function InputGroupInput({
export function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}: InputProps): React.ReactElement {
return <Input className={className} unstyled {...props} />;
}
function InputGroupTextarea({
export function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2.5 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}: TextareaProps): React.ReactElement {
return <Textarea className={className} unstyled {...props} />;
}
+62 -14
View File
@@ -1,20 +1,68 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
"use client";
import { cn } from "@/lib/utils"
import { Input as InputPrimitive } from "@base-ui/react/input";
import type * as React from "react";
import { cn } from "@/lib/utils";
export type InputProps = Omit<
InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
"size"
> & {
size?: "sm" | "default" | "lg" | number;
unstyled?: boolean;
nativeInput?: boolean;
};
export function Input({
className,
size = "default",
unstyled = false,
nativeInput = false,
style,
...props
}: InputProps): React.ReactElement {
const inputClassName = cn(
"h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none [transition:background-color_5000000s_ease-in-out_0s] placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5",
size === "sm" &&
"h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
props.type === "search" &&
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
props.type === "file" &&
"text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
);
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-3xl border border-transparent bg-input/50 px-3 py-1 text-base transition-[color,box-shadow,background-color] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
<span
className={
cn(
!unstyled &&
"relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
) || undefined
}
data-size={size}
data-slot="input-control"
>
{nativeInput ? (
<input
className={inputClassName}
data-slot="input"
size={typeof size === "number" ? size : undefined}
style={typeof style === "function" ? undefined : style}
{...props}
/>
) : (
<InputPrimitive
className={inputClassName}
data-slot="input"
size={typeof size === "number" ? size : undefined}
style={style}
{...props}
/>
)}
{...props}
/>
)
</span>
);
}
export { Input }
export { InputPrimitive };
+22 -16
View File
@@ -1,20 +1,26 @@
"use client"
"use client";
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
export function Label({
className,
render,
...props
}: useRender.ComponentProps<"label">): React.ReactElement {
const defaultProps = {
className: cn(
"inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4",
className,
),
"data-slot": "label",
};
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
return useRender({
defaultTagName: "label",
props: mergeProps<"label">(defaultProps, props),
render,
});
}
export { Label }
+327
View File
@@ -0,0 +1,327 @@
"use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { ChevronRightIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
export const MenuCreateHandle: typeof MenuPrimitive.createHandle =
MenuPrimitive.createHandle;
export const Menu: typeof MenuPrimitive.Root = MenuPrimitive.Root;
export const MenuPortal: typeof MenuPrimitive.Portal = MenuPrimitive.Portal;
export function MenuTrigger({
className,
children,
...props
}: MenuPrimitive.Trigger.Props): React.ReactElement {
return (
<MenuPrimitive.Trigger
className={className}
data-slot="menu-trigger"
{...props}
>
{children}
</MenuPrimitive.Trigger>
);
}
export function MenuPopup({
children,
className,
sideOffset = 4,
align = "center",
alignOffset,
side = "bottom",
anchor,
portalProps,
...props
}: MenuPrimitive.Popup.Props & {
align?: MenuPrimitive.Positioner.Props["align"];
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
side?: MenuPrimitive.Positioner.Props["side"];
anchor?: MenuPrimitive.Positioner.Props["anchor"];
portalProps?: MenuPrimitive.Portal.Props;
}): React.ReactElement {
return (
<MenuPortal {...portalProps}>
<MenuPrimitive.Positioner
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="z-50"
data-slot="menu-positioner"
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
className={cn(
"relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 outline-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
data-slot="menu-popup"
{...props}
>
<div className="max-h-(--available-height) w-full overflow-y-auto p-1">
{children}
</div>
</MenuPrimitive.Popup>
</MenuPrimitive.Positioner>
</MenuPortal>
);
}
export function MenuGroup(
props: MenuPrimitive.Group.Props,
): React.ReactElement {
return <MenuPrimitive.Group data-slot="menu-group" {...props} />;
}
export function MenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}): React.ReactElement {
return (
<MenuPrimitive.Item
className={cn(
"flex min-h-8 cursor-default select-none items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-inset:ps-8 data-[variant=destructive]:text-destructive-foreground data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:-mx-0.5 [&>svg]:shrink-0",
className,
)}
data-inset={inset}
data-slot="menu-item"
data-variant={variant}
{...props}
/>
);
}
export function MenuCheckboxItem({
className,
children,
checked,
variant = "default",
...props
}: MenuPrimitive.CheckboxItem.Props & {
variant?: "default" | "switch";
}): React.ReactElement {
return (
<MenuPrimitive.CheckboxItem
checked={checked}
className={cn(
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 ps-2 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variant === "switch"
? "grid-cols-[1fr_auto] gap-4 pe-1.5"
: "grid-cols-[.75rem_1fr] pe-4",
className,
)}
data-slot="menu-checkbox-item"
{...props}
>
{variant === "switch" ? (
<>
<span className="col-start-1">{children}</span>
<MenuPrimitive.CheckboxItemIndicator
className="inset-shadow-[0_1px_--theme(--color-black/4%)] inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(4)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(3)]"
keepMounted
>
<span className="pointer-events-none block aspect-square h-full in-[[data-slot=menu-checkbox-item][data-checked]]:origin-[var(--thumb-size)_50%] origin-left in-[[data-slot=menu-checkbox-item][data-checked]]:translate-x-[calc(var(--thumb-size)-4px)] in-[[data-slot=menu-checkbox-item]:active]:not-data-disabled:scale-x-110 in-[[data-slot=menu-checkbox-item]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.10)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s]" />
</MenuPrimitive.CheckboxItemIndicator>
</>
) : (
<>
<MenuPrimitive.CheckboxItemIndicator className="col-start-1 -ms-0.5">
<svg
aria-hidden="true"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</MenuPrimitive.CheckboxItemIndicator>
<span className="col-start-2">{children}</span>
</>
)}
</MenuPrimitive.CheckboxItem>
);
}
export function MenuRadioGroup(
props: MenuPrimitive.RadioGroup.Props,
): React.ReactElement {
return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
}
export function MenuRadioItem({
className,
children,
...props
}: MenuPrimitive.RadioItem.Props): React.ReactElement {
return (
<MenuPrimitive.RadioItem
className={cn(
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[.75rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="menu-radio-item"
{...props}
>
<MenuPrimitive.RadioItemIndicator className="col-start-1 -ms-0.5">
<svg
aria-hidden="true"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</MenuPrimitive.RadioItemIndicator>
<span className="col-start-2">{children}</span>
</MenuPrimitive.RadioItem>
);
}
export function MenuGroupLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}): React.ReactElement {
return (
<MenuPrimitive.GroupLabel
className={cn(
"px-2 py-1.5 font-medium text-muted-foreground text-xs data-inset:ps-9 sm:data-inset:ps-8",
className,
)}
data-inset={inset}
data-slot="menu-label"
{...props}
/>
);
}
export function MenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props): React.ReactElement {
return (
<MenuPrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border", className)}
data-slot="menu-separator"
{...props}
/>
);
}
export function MenuShortcut({
className,
...props
}: React.ComponentProps<"kbd">): React.ReactElement {
return (
<kbd
className={cn(
"ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
className,
)}
data-slot="menu-shortcut"
{...props}
/>
);
}
export function MenuSub(
props: MenuPrimitive.SubmenuRoot.Props,
): React.ReactElement {
return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />;
}
export function MenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}): React.ReactElement {
return (
<MenuPrimitive.SubmenuTrigger
className={cn(
"flex min-h-8 items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>svg:not(:last-child)]:-mx-0.5 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className,
)}
data-inset={inset}
data-slot="menu-sub-trigger"
{...props}
>
{children}
<ChevronRightIcon className="ms-auto -me-0.5 opacity-80" />
</MenuPrimitive.SubmenuTrigger>
);
}
export function MenuSubPopup({
className,
sideOffset = 0,
alignOffset,
align = "start",
...props
}: MenuPrimitive.Popup.Props & {
align?: MenuPrimitive.Positioner.Props["align"];
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
}): React.ReactElement {
const defaultAlignOffset = align !== "center" ? -5 : undefined;
return (
<MenuPopup
align={align}
alignOffset={alignOffset ?? defaultAlignOffset}
className={className}
data-slot="menu-sub-content"
side="inline-end"
sideOffset={sideOffset}
{...props}
/>
);
}
export {
MenuPrimitive,
MenuCreateHandle as DropdownMenuCreateHandle,
Menu as DropdownMenu,
MenuPortal as DropdownMenuPortal,
MenuTrigger as DropdownMenuTrigger,
MenuPopup as DropdownMenuContent,
MenuGroup as DropdownMenuGroup,
MenuItem as DropdownMenuItem,
MenuCheckboxItem as DropdownMenuCheckboxItem,
MenuRadioGroup as DropdownMenuRadioGroup,
MenuRadioItem as DropdownMenuRadioItem,
MenuGroupLabel as DropdownMenuLabel,
MenuSeparator as DropdownMenuSeparator,
MenuShortcut as DropdownMenuShortcut,
MenuSub as DropdownMenuSub,
MenuSubTrigger as DropdownMenuSubTrigger,
MenuSubPopup as DropdownMenuSubContent,
};
+87 -59
View File
@@ -1,90 +1,118 @@
"use client"
"use client";
import * as React from "react"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
export const PopoverCreateHandle: typeof PopoverPrimitive.createHandle =
PopoverPrimitive.createHandle;
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
export const Popover: typeof PopoverPrimitive.Root = PopoverPrimitive.Root;
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
export function PopoverTrigger({
className,
align = "center",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
children,
...props
}: PopoverPrimitive.Popup.Props &
Pick<
PopoverPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
}: PopoverPrimitive.Trigger.Props): React.ReactElement {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Trigger
className={className}
data-slot="popover-trigger"
{...props}
>
{children}
</PopoverPrimitive.Trigger>
);
}
export function PopoverPopup({
children,
className,
side = "bottom",
align = "center",
sideOffset = 4,
alignOffset = 0,
tooltipStyle = false,
anchor,
portalProps,
...props
}: PopoverPrimitive.Popup.Props & {
portalProps?: PopoverPrimitive.Portal.Props;
side?: PopoverPrimitive.Positioner.Props["side"];
align?: PopoverPrimitive.Positioner.Props["align"];
sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"];
alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"];
tooltipStyle?: boolean;
anchor?: PopoverPrimitive.Positioner.Props["anchor"];
}): React.ReactElement {
return (
<PopoverPrimitive.Portal {...portalProps}>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
data-slot="popover-positioner"
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<PopoverPrimitive.Popup
data-slot="popover-content"
className={cn(
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-3xl bg-popover p-4 text-sm text-popover-foreground shadow-lg ring-1 ring-foreground/5 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
"relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 outline-none transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
tooltipStyle &&
"w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",
className,
)}
data-slot="popover-popup"
{...props}
/>
>
<PopoverPrimitive.Viewport
className={cn(
"relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) py-4 [--viewport-inline-padding:--spacing(4)] has-data-[slot=calendar]:p-2 data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity",
tooltipStyle
? "py-1 [--viewport-inline-padding:--spacing(2)]"
: "not-data-transitioning:overflow-y-auto",
)}
data-slot="popover-viewport"
>
{children}
</PopoverPrimitive.Viewport>
</PopoverPrimitive.Popup>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
)
);
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-1 text-sm", className)}
{...props}
/>
)
export function PopoverClose({
...props
}: PopoverPrimitive.Close.Props): React.ReactElement {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
}
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return (
<PopoverPrimitive.Title
data-slot="popover-title"
className={cn("text-base font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
export function PopoverTitle({
className,
...props
}: PopoverPrimitive.Description.Props) {
}: PopoverPrimitive.Title.Props): React.ReactElement {
return (
<PopoverPrimitive.Description
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
<PopoverPrimitive.Title
className={cn("font-semibold text-lg leading-none", className)}
data-slot="popover-title"
{...props}
/>
)
);
}
export {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
export function PopoverDescription({
className,
...props
}: PopoverPrimitive.Description.Props): React.ReactElement {
return (
<PopoverPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="popover-description"
{...props}
/>
);
}
export { PopoverPrimitive, PopoverPopup as PopoverContent };
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
import type React from "react";
import { cn } from "@/lib/utils";
export const PreviewCard: typeof PreviewCardPrimitive.Root =
PreviewCardPrimitive.Root;
export function PreviewCardTrigger({
...props
}: PreviewCardPrimitive.Trigger.Props): React.ReactElement {
return (
<PreviewCardPrimitive.Trigger data-slot="preview-card-trigger" {...props} />
);
}
export function PreviewCardPopup({
className,
children,
align = "center",
sideOffset = 4,
anchor,
portalProps,
...props
}: PreviewCardPrimitive.Popup.Props & {
align?: PreviewCardPrimitive.Positioner.Props["align"];
sideOffset?: PreviewCardPrimitive.Positioner.Props["sideOffset"];
anchor?: PreviewCardPrimitive.Positioner.Props["anchor"];
portalProps?: PreviewCardPrimitive.Portal.Props;
}): React.ReactElement {
return (
<PreviewCardPrimitive.Portal {...portalProps}>
<PreviewCardPrimitive.Positioner
align={align}
anchor={anchor}
className="z-50"
data-slot="preview-card-positioner"
sideOffset={sideOffset}
>
<PreviewCardPrimitive.Popup
className={cn(
"relative flex w-64 origin-(--transform-origin) text-balance rounded-lg border bg-popover not-dark:bg-clip-padding p-4 text-popover-foreground text-sm shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
data-slot="preview-card-content"
{...props}
>
{children}
</PreviewCardPrimitive.Popup>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
);
}
export {
PreviewCardPrimitive,
PreviewCard as HoverCard,
PreviewCardTrigger as HoverCardTrigger,
PreviewCardPopup as HoverCardContent,
};
+46 -45
View File
@@ -1,83 +1,84 @@
"use client"
"use client";
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
function Progress({
export function Progress({
className,
children,
value,
...props
}: ProgressPrimitive.Root.Props) {
}: ProgressPrimitive.Root.Props): React.ReactElement {
return (
<ProgressPrimitive.Root
value={value}
className={cn("flex w-full flex-col gap-2", className)}
data-slot="progress"
className={cn("flex flex-wrap gap-3", className)}
{...props}
>
{children}
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
{children ? (
children
) : (
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
)}
</ProgressPrimitive.Root>
)
);
}
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
export function ProgressLabel({
className,
...props
}: ProgressPrimitive.Label.Props): React.ReactElement {
return (
<ProgressPrimitive.Label
className={cn("font-medium text-sm", className)}
data-slot="progress-label"
{...props}
/>
);
}
export function ProgressTrack({
className,
...props
}: ProgressPrimitive.Track.Props): React.ReactElement {
return (
<ProgressPrimitive.Track
className={cn(
"relative flex h-3 w-full items-center overflow-x-hidden rounded-full bg-muted",
className
"block h-1.5 w-full overflow-hidden rounded-full bg-input",
className,
)}
data-slot="progress-track"
{...props}
/>
)
);
}
function ProgressIndicator({
export function ProgressIndicator({
className,
...props
}: ProgressPrimitive.Indicator.Props) {
}: ProgressPrimitive.Indicator.Props): React.ReactElement {
return (
<ProgressPrimitive.Indicator
className={cn("bg-primary transition-all duration-500", className)}
data-slot="progress-indicator"
className={cn("h-full bg-primary transition-all", className)}
{...props}
/>
)
);
}
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return (
<ProgressPrimitive.Label
className={cn("text-sm font-medium", className)}
data-slot="progress-label"
{...props}
/>
)
}
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
export function ProgressValue({
className,
...props
}: ProgressPrimitive.Value.Props): React.ReactElement {
return (
<ProgressPrimitive.Value
className={cn(
"ml-auto text-sm text-muted-foreground tabular-nums",
className
)}
className={cn("text-sm tabular-nums", className)}
data-slot="progress-value"
{...props}
/>
)
);
}
export {
Progress,
ProgressTrack,
ProgressIndicator,
ProgressLabel,
ProgressValue,
}
export { ProgressPrimitive };
+43 -24
View File
@@ -1,55 +1,74 @@
"use client"
"use client";
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
function ScrollArea({
export function ScrollArea({
className,
children,
scrollFade = false,
scrollbarGutter = false,
fill = false,
clampContentMinWidth = true,
...props
}: ScrollAreaPrimitive.Root.Props) {
}: ScrollAreaPrimitive.Root.Props & {
scrollFade?: boolean;
scrollbarGutter?: boolean;
fill?: boolean;
clampContentMinWidth?: boolean;
}): React.ReactElement {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
className={cn("size-full min-h-0", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
className={cn(
"h-full rounded-[inherit] outline-none transition-shadows focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-has-overflow-y:overscroll-y-contain data-has-overflow-x:overscroll-x-contain",
scrollFade &&
"mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]",
scrollbarGutter &&
"data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
)}
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
<ScrollAreaPrimitive.Content
className={cn(fill && "size-full")}
data-slot="scroll-area-content"
style={clampContentMinWidth ? { minWidth: 0 } : undefined}
>
{children}
</ScrollAreaPrimitive.Content>
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
<ScrollBar orientation="vertical" />
<ScrollBar orientation="horizontal" />
<ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
</ScrollAreaPrimitive.Root>
)
);
}
function ScrollBar({
export function ScrollBar({
className,
orientation = "vertical",
...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
}: ScrollAreaPrimitive.Scrollbar.Props): React.ReactElement {
return (
<ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
className
"m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
className,
)}
data-slot="scroll-area-scrollbar"
orientation={orientation}
{...props}
>
<ScrollAreaPrimitive.Thumb
className="relative flex-1 rounded-full bg-foreground/20"
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.Scrollbar>
)
);
}
export { ScrollArea, ScrollBar }
export { ScrollAreaPrimitive };
+181 -128
View File
@@ -1,204 +1,257 @@
"use client"
"use client";
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { mergeProps } from "@base-ui/react/merge-props";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import {
ChevronDownIcon,
ChevronsUpDownIcon,
ChevronUpIcon,
} from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
export const Select: typeof SelectPrimitive.Root = SelectPrimitive.Root;
const Select = SelectPrimitive.Root
export const selectTriggerVariants = cva(
"relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-between gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
{
defaultVariants: {
size: "default",
},
variants: {
size: {
default: "",
lg: "min-h-10 sm:min-h-9",
sm: "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
},
},
},
);
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1.5 p-1.5", className)}
{...props}
/>
)
export const selectTriggerIconClassName = "-me-1 size-4.5 opacity-80 sm:size-4";
export interface SelectButtonProps extends useRender.ComponentProps<"button"> {
size?: VariantProps<typeof selectTriggerVariants>["size"];
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
export function SelectButton({
className,
size,
render,
children,
...props
}: SelectButtonProps): React.ReactElement {
const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
render ? undefined : "button";
const defaultProps = {
children: (
<>
<span className="flex-1 truncate in-data-placeholder:text-muted-foreground/72">
{children}
</span>
<ChevronsUpDownIcon className={selectTriggerIconClassName} />
</>
),
className: cn(selectTriggerVariants({ size }), "min-w-0", className),
"data-slot": "select-button",
type: typeValue,
};
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(defaultProps, props),
render,
});
}
function SelectTrigger({
export function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
}: SelectPrimitive.Trigger.Props &
VariantProps<typeof selectTriggerVariants>): React.ReactElement {
return (
<SelectPrimitive.Trigger
className={cn(selectTriggerVariants({ size }), className)}
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-3xl border border-transparent bg-input/50 px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow,background-color] outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
}
/>
<SelectPrimitive.Icon data-slot="select-icon">
<ChevronsUpDownIcon className={selectTriggerIconClassName} />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
export function SelectValue({
className,
...props
}: SelectPrimitive.Value.Props): React.ReactElement {
return (
<SelectPrimitive.Value
className={cn(
"flex-1 truncate data-placeholder:text-muted-foreground",
className,
)}
data-slot="select-value"
{...props}
/>
);
}
export function SelectPopup({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
align = "start",
alignOffset = 0,
alignItemWithTrigger = true,
anchor,
portalProps,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
}: SelectPrimitive.Popup.Props & {
portalProps?: SelectPrimitive.Portal.Props;
side?: SelectPrimitive.Positioner.Props["side"];
sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
align?: SelectPrimitive.Positioner.Props["align"];
alignOffset?: SelectPrimitive.Positioner.Props["alignOffset"];
alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"];
anchor?: SelectPrimitive.Positioner.Props["anchor"];
}): React.ReactElement {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Portal {...portalProps}>
<SelectPrimitive.Positioner
align={align}
alignItemWithTrigger={alignItemWithTrigger}
alignOffset={alignOffset}
anchor={anchor}
className="z-50 select-none"
data-slot="select-positioner"
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-3xl bg-popover text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
className="origin-(--transform-origin) text-foreground outline-none"
data-slot="select-popup"
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
<SelectPrimitive.ScrollUpArrow
className="top-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:top-px before:h-[200%] before:rounded-t-[calc(var(--radius-lg)-1px)] before:bg-linear-to-b before:from-50% before:from-popover"
data-slot="select-scroll-up-arrow"
>
<ChevronUpIcon className="relative size-4.5 sm:size-4" />
</SelectPrimitive.ScrollUpArrow>
<div className="relative h-full min-w-(--anchor-width) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]">
<SelectPrimitive.List
className={cn(
"max-h-(--available-height) overflow-y-auto p-1",
className,
)}
data-slot="select-list"
>
{children}
</SelectPrimitive.List>
</div>
<SelectPrimitive.ScrollDownArrow
className="bottom-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:bottom-px before:h-[200%] before:rounded-b-[calc(var(--radius-lg)-1px)] before:bg-linear-to-t before:from-50% before:from-popover"
data-slot="select-scroll-down-arrow"
>
<ChevronDownIcon className="relative size-4.5 sm:size-4" />
</SelectPrimitive.ScrollDownArrow>
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-3 py-2.5 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
export function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
}: SelectPrimitive.Item.Props): React.ReactElement {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="select-item"
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
<SelectPrimitive.ItemIndicator className="col-start-1">
<svg
aria-hidden="true"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="col-start-2 min-w-0">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
export function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
}: SelectPrimitive.Separator.Props): React.ReactElement {
return (
<SelectPrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border", className)}
data-slot="select-separator"
className={cn(
"pointer-events-none -mx-1.5 my-1.5 h-px bg-border",
className
)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
export function SelectGroup(
props: SelectPrimitive.Group.Props,
): React.ReactElement {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
export function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
}: SelectPrimitive.Label.Props): React.ReactElement {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
<SelectPrimitive.Label
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
"not-in-data-[slot=field]:mb-2 inline-flex cursor-default items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4",
className,
)}
data-slot="select-label"
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
/>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
export function SelectGroupLabel(
props: SelectPrimitive.GroupLabel.Props,
): React.ReactElement {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
<SelectPrimitive.GroupLabel
className="px-2 py-1.5 font-medium text-muted-foreground text-xs"
data-slot="select-group-label"
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
/>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
export { SelectPrimitive, SelectPopup as SelectContent };
+11 -13
View File
@@ -1,25 +1,23 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import type React from "react";
import { cn } from "@/lib/utils";
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
export function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
}: SeparatorPrimitive.Props): React.ReactElement {
return (
<SeparatorPrimitive
className={cn(
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:not-[[class^='h-']]:not-[[class*='_h-']]:self-stretch",
className,
)}
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
);
}
export { Separator }
export { SeparatorPrimitive };
+195 -102
View File
@@ -1,138 +1,231 @@
"use client"
"use client";
import * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { XIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
export const Sheet: typeof SheetPrimitive.Root = SheetPrimitive.Root;
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
export const SheetPortal: typeof SheetPrimitive.Portal = SheetPrimitive.Portal;
export function SheetTrigger(
props: SheetPrimitive.Trigger.Props,
): React.ReactElement {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
export function SheetClose(
props: SheetPrimitive.Close.Props,
): React.ReactElement {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
export function SheetBackdrop({
className,
...props
}: SheetPrimitive.Backdrop.Props): React.ReactElement {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/30 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-sm",
className
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="sheet-backdrop"
{...props}
/>
)
);
}
function SheetContent({
export function SheetViewport({
className,
side,
variant = "default",
...props
}: SheetPrimitive.Viewport.Props & {
side?: "right" | "left" | "top" | "bottom";
variant?: "default" | "inset";
}): React.ReactElement {
return (
<SheetPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 grid",
side === "bottom" && "grid grid-rows-[1fr_auto] pt-12",
side === "top" && "grid grid-rows-[auto_1fr] pb-12",
side === "left" && "flex justify-start",
side === "right" && "flex justify-end",
variant === "inset" && "sm:p-4",
className,
)}
data-slot="sheet-viewport"
{...props}
/>
);
}
export function SheetPopup({
className,
children,
side = "right",
showCloseButton = true,
side = "right",
variant = "default",
closeProps,
portalProps,
...props
}: SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
showCloseButton?: boolean;
side?: "right" | "left" | "top" | "bottom";
variant?: "default" | "inset";
closeProps?: SheetPrimitive.Close.Props;
portalProps?: SheetPrimitive.Portal.Props;
}): React.ReactElement {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col bg-popover bg-clip-padding text-sm text-popover-foreground shadow-xl transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={
<Button
variant="ghost"
className="absolute top-4 right-4 bg-secondary"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
<SheetPortal {...portalProps}>
<SheetBackdrop />
<SheetViewport side={side} variant={variant}>
<SheetPrimitive.Popup
className={cn(
"relative flex max-h-full min-h-0 w-full min-w-0 flex-col bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:before:hidden dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
side === "bottom" &&
"row-start-2 border-t data-ending-style:translate-y-8 data-starting-style:translate-y-8",
side === "top" &&
"border-b data-ending-style:-translate-y-8 data-starting-style:-translate-y-8",
side === "left" &&
"w-[calc(100%-(--spacing(12)))] max-w-md border-e data-ending-style:-translate-x-8 data-starting-style:-translate-x-8",
side === "right" &&
"col-start-2 w-[calc(100%-(--spacing(12)))] max-w-md border-s data-ending-style:translate-x-8 data-starting-style:translate-x-8",
variant === "inset" &&
"before:hidden sm:rounded-2xl sm:border sm:before:rounded-[calc(var(--radius-2xl)-1px)] sm:**:data-[slot=sheet-footer]:rounded-b-[calc(var(--radius-2xl)-1px)]",
className,
)}
data-slot="sheet-popup"
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
aria-label="Close"
className="absolute end-2 top-2"
render={<Button size="icon" variant="ghost" />}
{...closeProps}
>
<XIcon />
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetViewport>
</SheetPortal>
)
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-6", className)}
{...props}
/>
)
export function SheetHeader({
className,
render,
...props
}: useRender.ComponentProps<"div">): React.ReactElement {
const defaultProps = {
className: cn(
"flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4",
className,
),
"data-slot": "sheet-header",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-6", className)}
{...props}
/>
)
export function SheetFooter({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"div"> & {
variant?: "default" | "bare";
}): React.ReactElement {
const defaultProps = {
className: cn(
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end",
variant === "default" && "border-t bg-muted/72 py-4",
variant === "bare" &&
"in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pt-3 pt-4 pb-6",
className,
),
"data-slot": "sheet-footer",
};
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
});
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"font-heading text-base font-medium text-foreground",
className
)}
{...props}
/>
)
}
function SheetDescription({
export function SheetTitle({
className,
...props
}: SheetPrimitive.Description.Props) {
}: SheetPrimitive.Title.Props): React.ReactElement {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
<SheetPrimitive.Title
className={cn(
"font-heading font-semibold text-xl leading-none",
className,
)}
data-slot="sheet-title"
{...props}
/>
)
);
}
export function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props): React.ReactElement {
return (
<SheetPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="sheet-description"
{...props}
/>
);
}
export function SheetPanel({
className,
scrollFade = true,
render,
...props
}: useRender.ComponentProps<"div"> & {
scrollFade?: boolean;
}): React.ReactElement {
const defaultProps = {
className: cn(
"p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-header])]:pt-1 in-[[data-slot=sheet-popup]:has([data-slot=sheet-footer]:not(.border-t))]:pb-1",
className,
),
"data-slot": "sheet-panel",
};
return (
<ScrollArea scrollFade={scrollFade}>
{useRender({
defaultTagName: "div",
props: mergeProps<"div">(defaultProps, props),
render,
})}
</ScrollArea>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
SheetPrimitive,
SheetBackdrop as SheetOverlay,
SheetPopup as SheetContent,
};
File diff suppressed because it is too large Load Diff
+11 -6
View File
@@ -1,13 +1,18 @@
import { cn } from "@/lib/utils"
import type React from "react";
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
export function Skeleton({
className,
...props
}: React.ComponentProps<"div">): React.ReactElement {
return (
<div
className={cn(
"animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0/200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
className,
)}
data-slot="skeleton"
className={cn("animate-pulse rounded-2xl bg-muted", className)}
{...props}
/>
)
);
}
export { Skeleton }
+14 -7
View File
@@ -1,10 +1,17 @@
import { cn } from "@/lib/utils"
import { Loader2Icon } from "lucide-react"
import { Loader2Icon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
export function Spinner({
className,
...props
}: React.ComponentProps<typeof Loader2Icon>): React.ReactElement {
return (
<Loader2Icon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
)
<Loader2Icon
aria-label="Loading"
className={cn("animate-spin", className)}
role="status"
{...props}
/>
);
}
export { Spinner }
+14 -16
View File
@@ -1,32 +1,30 @@
"use client"
"use client";
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
function Switch({
export function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
}: SwitchPrimitive.Root.Props): React.ReactElement {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border-2 transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-5 data-[size=default]:w-11 data-[size=sm]:h-4 data-[size=sm]:w-7 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-unchecked:border-transparent data-unchecked:bg-input/90 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
"inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(5)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-disabled:cursor-not-allowed data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(4)]",
className,
)}
data-slot="switch"
{...props}
>
<SwitchPrimitive.Thumb
className={cn(
"pointer-events-none block aspect-square h-full origin-left in-[[role=switch]:active,[data-slot=label]:active,[data-slot=field-label]:active]:not-data-disabled:scale-x-110 in-[[role=switch]:active,[data-slot=label]:active,[data-slot=field-label]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.1)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s] data-checked:origin-[var(--thumb-size)_50%] data-checked:translate-x-[calc(var(--thumb-size)-4px)]",
)}
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background shadow-sm ring-0 transition-transform not-dark:bg-clip-padding group-data-[size=default]/switch:h-4 group-data-[size=default]/switch:w-6 group-data-[size=sm]/switch:h-3 group-data-[size=sm]/switch:w-4 data-checked:translate-x-[calc(100%-8px)] dark:data-checked:bg-primary-foreground data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
);
}
export { Switch }
export { SwitchPrimitive };
+53 -46
View File
@@ -1,82 +1,89 @@
"use client"
"use client";
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
export type TabsVariant = "default" | "underline";
function Tabs({
export function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
}: TabsPrimitive.Root.Props): React.ReactElement {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
"flex flex-col gap-2 data-[orientation=vertical]:flex-row",
className,
)}
data-slot="tabs"
{...props}
/>
)
);
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-full p-1 text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
export function TabsList({
variant = "default",
className,
children,
...props
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
}: TabsPrimitive.List.Props & {
variant?: TabsVariant;
}): React.ReactElement {
return (
<TabsPrimitive.List
className={cn(
"relative z-0 flex w-fit items-center justify-center gap-x-0.5 text-muted-foreground",
"data-[orientation=vertical]:flex-col",
variant === "default"
? "rounded-lg bg-muted p-0.5 text-muted-foreground/72"
: "data-[orientation=vertical]:px-1 data-[orientation=horizontal]:py-1 *:data-[slot=tabs-tab]:hover:bg-accent",
className,
)}
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
>
{children}
<TabsPrimitive.Indicator
className={cn(
"absolute bottom-0 left-0 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) -translate-y-(--active-tab-bottom) transition-[width,translate] duration-200 ease-in-out",
variant === "underline"
? "z-10 bg-primary data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5 data-[orientation=vertical]:-translate-x-px data-[orientation=horizontal]:translate-y-px"
: "-z-1 rounded-md bg-background shadow-sm/5 dark:bg-input",
)}
data-slot="tab-indicator"
/>
</TabsPrimitive.List>
);
}
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
export function TabsTab({
className,
...props
}: TabsPrimitive.Tab.Props): React.ReactElement {
return (
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
"relative flex h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-[calc(--spacing(2.5)-1px)] font-medium text-base outline-none transition-[color,background-color,box-shadow] hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start data-active:text-foreground data-disabled:opacity-64 sm:h-8 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0",
className,
)}
data-slot="tabs-tab"
{...props}
/>
)
);
}
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
export function TabsPanel({
className,
...props
}: TabsPrimitive.Panel.Props): React.ReactElement {
return (
<TabsPrimitive.Panel
className={cn("flex-1 outline-none", className)}
data-slot="tabs-content"
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
export { TabsPrimitive, TabsTab as TabsTrigger, TabsPanel as TabsContent };
+53 -13
View File
@@ -1,18 +1,58 @@
import * as React from "react"
"use client";
import { cn } from "@/lib/utils"
import { Field as FieldPrimitive } from "@base-ui/react/field";
import { mergeProps } from "@base-ui/react/merge-props";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
export type TextareaProps = React.ComponentPropsWithoutRef<"textarea"> &
React.RefAttributes<HTMLTextAreaElement> & {
size?: "sm" | "default" | "lg" | number;
unstyled?: boolean;
};
export function Textarea({
className,
size = "default",
unstyled = false,
ref,
...props
}: TextareaProps): React.ReactElement {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full resize-none rounded-2xl border border-transparent bg-input/50 px-3 py-3 text-base transition-[color,box-shadow,background-color] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
<span
className={
cn(
!unstyled &&
"relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] sm:text-sm dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
) || undefined
}
data-size={size}
data-slot="textarea-control"
>
<FieldPrimitive.Control
ref={ref}
value={props.value}
defaultValue={props.defaultValue}
disabled={props.disabled}
id={props.id}
name={props.name}
render={(defaultProps: React.ComponentProps<"textarea">) => (
<textarea
className={cn(
"field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5",
size === "sm" &&
"min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
size === "lg" &&
"min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
)}
data-slot="textarea"
{...mergeProps(defaultProps, props)}
/>
)}
/>
</span>
);
}
export { Textarea }
export { FieldPrimitive };
+42 -41
View File
@@ -1,66 +1,67 @@
"use client"
"use client";
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import type React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
export const TooltipCreateHandle: typeof TooltipPrimitive.createHandle =
TooltipPrimitive.createHandle;
function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
)
export const TooltipProvider: typeof TooltipPrimitive.Provider =
TooltipPrimitive.Provider;
export const Tooltip: typeof TooltipPrimitive.Root = TooltipPrimitive.Root;
export function TooltipTrigger(
props: TooltipPrimitive.Trigger.Props,
): React.ReactElement {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
export function TooltipPopup({
className,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
sideOffset = 4,
side = "top",
anchor,
children,
portalProps,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
}: TooltipPrimitive.Popup.Props & {
align?: TooltipPrimitive.Positioner.Props["align"];
side?: TooltipPrimitive.Positioner.Props["side"];
sideOffset?: TooltipPrimitive.Positioner.Props["sideOffset"];
anchor?: TooltipPrimitive.Positioner.Props["anchor"];
portalProps?: TooltipPrimitive.Portal.Props;
}): React.ReactElement {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Portal {...portalProps}>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
data-slot="tooltip-positioner"
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-xl bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-lg data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
"relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) text-balance rounded-md border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs shadow-md/5 transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:duration-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
className,
)}
data-slot="tooltip-popup"
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:translate-x-[1.5px] data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:translate-x-[-1.5px] data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:translate-x-[-1.5px] data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:translate-x-[1.5px] data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
<TooltipPrimitive.Viewport
className="relative size-full overflow-clip px-(--viewport-inline-padding) py-1 [--viewport-inline-padding:--spacing(2)] data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:truncate **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity"
data-slot="tooltip-viewport"
>
{children}
</TooltipPrimitive.Viewport>
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
)
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { TooltipPrimitive, TooltipPopup as TooltipContent };
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { useCallback, useSyncExternalStore } from "react";
const BREAKPOINTS = {
"2xl": 1536,
"3xl": 1600,
"4xl": 2000,
lg: 1024,
md: 800,
sm: 640,
xl: 1280,
} as const;
type Breakpoint = keyof typeof BREAKPOINTS;
type BreakpointQuery =
| Breakpoint
| `max-${Breakpoint}`
| `${Breakpoint}:max-${Breakpoint}`;
function resolveMin(value: Breakpoint | number): string {
const px = typeof value === "number" ? value : BREAKPOINTS[value];
return `(min-width: ${px}px)`;
}
function resolveMax(value: Breakpoint | number): string {
const px = typeof value === "number" ? value : BREAKPOINTS[value];
return `(max-width: ${px - 1}px)`;
}
function parseQuery(
query: BreakpointQuery | MediaQueryInput | (string & {}),
): string {
if (typeof query !== "string") {
const parts: string[] = [];
if (query.min != null) parts.push(resolveMin(query.min));
if (query.max != null) parts.push(resolveMax(query.max));
if (query.pointer === "coarse") parts.push("(pointer: coarse)");
if (query.pointer === "fine") parts.push("(pointer: fine)");
if (parts.length === 0) return "(min-width: 0px)";
return parts.join(" and ");
}
if (query.startsWith("(")) return query;
const parts: string[] = [];
for (const segment of query.split(":")) {
if (segment.startsWith("max-")) {
const bp = segment.slice(4);
if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint));
} else if (segment in BREAKPOINTS) {
parts.push(resolveMin(segment as Breakpoint));
}
}
return parts.length > 0 ? parts.join(" and ") : query;
}
function getServerSnapshot(): boolean {
return false;
}
export type MediaQueryInput = {
min?: Breakpoint | number;
max?: Breakpoint | number;
/** Touch-like input (finger). Use "fine" for mouse/trackpad. */
pointer?: "coarse" | "fine";
};
export function useMediaQuery(
query: BreakpointQuery | MediaQueryInput | (string & {}),
): boolean {
const mediaQuery = parseQuery(query);
const subscribe = useCallback(
(callback: () => void) => {
if (typeof window === "undefined") return () => {};
const mql = window.matchMedia(mediaQuery);
mql.addEventListener("change", callback);
return () => mql.removeEventListener("change", callback);
},
[mediaQuery],
);
const getSnapshot = useCallback(() => {
if (typeof window === "undefined") return false;
return window.matchMedia(mediaQuery).matches;
}, [mediaQuery]);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
export function useIsMobile(): boolean {
return useMediaQuery("max-md");
}
+34
View File
@@ -0,0 +1,34 @@
"use client";
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import en from "./locales/en/translation.json";
export const defaultNS = "translation";
// Add new languages here (and a matching JSON under locales/<lng>/translation.json).
export const resources = {
en: { translation: en },
} as const;
if (!i18n.isInitialized) {
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
defaultNS,
fallbackLng: "en",
// Only `en` exists today; keep this in sync with `resources` as languages grow.
supportedLngs: ["en"],
interpolation: { escapeValue: false },
detection: {
order: ["localStorage", "navigator", "htmlTag"],
caches: ["localStorage"],
},
});
}
export default i18n;
@@ -0,0 +1,65 @@
{
"common": {
"appName": "temetro",
"email": "Email",
"password": "Password",
"signIn": "Sign in",
"signUp": "Sign up"
},
"auth": {
"login": {
"title": "Welcome back",
"subtitle": "Sign in to your clinician account",
"emailLabel": "Email",
"emailPlaceholder": "you@clinic.org",
"passwordLabel": "Password",
"forgotPassword": "Forgot your password?",
"submit": "Sign in",
"submitting": "Signing in…",
"noAccount": "Don't have an account?",
"signUpLink": "Sign up",
"error": "Could not sign in. Check your email and password and try again."
},
"signup": {
"title": "Create your account",
"subtitle": "Start using temetro in your clinic",
"nameLabel": "Full name",
"namePlaceholder": "Dr. Jane Okafor",
"emailLabel": "Email",
"emailPlaceholder": "you@clinic.org",
"passwordLabel": "Password",
"confirmPasswordLabel": "Confirm password",
"passwordHint": "Must be at least {{count}} characters long.",
"passwordTooShort": "Password must be at least {{count}} characters.",
"passwordMismatch": "Passwords do not match.",
"submit": "Create account",
"submitting": "Creating account…",
"haveAccount": "Already have an account?",
"signInLink": "Sign in",
"error": "Could not create your account."
}
},
"nav": {
"newChat": "New chat",
"patients": "Patients",
"settings": "Settings",
"notifications": "Notifications",
"viewAllNotifications": "View all notifications"
},
"settings": {
"tabs": {
"profile": "Profile",
"records": "Records",
"signing": "Signing",
"careTeam": "Care team",
"developers": "Developers"
},
"empty": "Nothing here yet.",
"records": {
"description": "How patient records are sourced, stored, and displayed"
},
"developers": {
"description": "Access tokens for the temetro API"
}
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
+96
View File
@@ -24,13 +24,17 @@
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.40.0",
"i18next": "^26.3.1",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.17.0",
"media-chrome": "^4.19.0",
"motion": "^12.40.0",
"nanoid": "^5.1.11",
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-i18next": "^17.0.8",
"react-jsx-parser": "^2.4.1",
"shadcn": "^4.8.3",
"shiki": "^4.1.0",
@@ -8280,6 +8284,15 @@
"node": ">=16.9.0"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -8342,6 +8355,43 @@
"node": ">=18.18.0"
}
},
"node_modules/i18next": {
"version": "26.3.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"peerDependencies": {
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@@ -11096,6 +11146,16 @@
}
}
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
@@ -12009,6 +12069,33 @@
"react": "^19.2.4"
}
},
"node_modules/react-i18next": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 26.2.0",
"react": ">= 16.8.0",
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -14323,6 +14410,15 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+4
View File
@@ -25,13 +25,17 @@
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.40.0",
"i18next": "^26.3.1",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.17.0",
"media-chrome": "^4.19.0",
"motion": "^12.40.0",
"nanoid": "^5.1.11",
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-i18next": "^17.0.8",
"react-jsx-parser": "^2.4.1",
"shadcn": "^4.8.3",
"shiki": "^4.1.0",