mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
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:
+29
-11
@@ -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 (non–ai-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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Button as InputGroupButton } from "@/components/ui/button";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/components/ui/input-group";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 & 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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 }
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
+397
-383
File diff suppressed because it is too large
Load Diff
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Generated
+96
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user