From 3dc4d1415ae21121d07ef08cf1c4cbe2e8c97c83 Mon Sep 17 00:00:00 2001 From: Johnny Bouder Date: Fri, 4 Sep 2026 12:31:46 -0400 Subject: [PATCH 1/3] chore(ui): move components to the src/components/ui alias Issue nebari-dev/nebari-design#143 calls out the `ui` alias drift in this repo: components.json pointed `ui` at `@/ui`, so registry components lived in src/ui/ instead of the conventional src/components/ui/. Move them, point the alias at `@/components/ui`, and rewrite every import. Also give the solution-style tsconfig.json the `@/*` path mapping, since `shadcn add` reads aliases from it and otherwise writes files to a literal `@/` directory. While here, separate app-owned code from registry-owned files so the registry files can be byte-identical: - avatar.tsx is not a registry item (there is no @nebari/avatar); move it to src/components/avatar.tsx. - copyText() lived in src/lib/utils.ts on top of the registry cn() helper; move it to src/lib/clipboard.ts. Co-Authored-By: Claude Fable 5.1 --- ui/components.json | 2 +- ui/src/components/app-bits.tsx | 12 ++++----- ui/src/components/app-form.tsx | 14 +++++------ ui/src/{ui => components}/avatar.tsx | 0 ui/src/components/header.tsx | 6 ++--- ui/src/components/layout.tsx | 2 +- ui/src/components/onboarding.tsx | 8 +++--- ui/src/{ => components}/ui/alert.tsx | 0 ui/src/{ => components}/ui/badge.tsx | 0 ui/src/{ => components}/ui/button.tsx | 2 +- ui/src/{ => components}/ui/card.tsx | 0 ui/src/{ => components}/ui/checkbox.tsx | 0 ui/src/{ => components}/ui/code-block.tsx | 3 ++- ui/src/{ => components}/ui/dialog.tsx | 2 +- ui/src/{ => components}/ui/dropdown-menu.tsx | 2 +- ui/src/{ => components}/ui/field.tsx | 0 ui/src/{ => components}/ui/input.tsx | 0 ui/src/{ => components}/ui/label.tsx | 0 .../{ => components}/ui/navigation-menu.tsx | 2 +- ui/src/{ => components}/ui/radio-group.tsx | 0 ui/src/{ => components}/ui/select.tsx | 0 ui/src/{ => components}/ui/slider.tsx | 0 ui/src/{ => components}/ui/spinner.tsx | 0 ui/src/{ => components}/ui/switch.tsx | 0 ui/src/{ => components}/ui/table.tsx | 0 ui/src/{ => components}/ui/tabs.tsx | 0 ui/src/{ => components}/ui/textarea.tsx | 0 ui/src/{ => components}/ui/toast.tsx | 0 ui/src/{ => components}/ui/tooltip.tsx | 0 ui/src/lib/clipboard.ts | 24 ++++++++++++++++++ ui/src/lib/utils.ts | 25 ------------------- ui/src/main.tsx | 2 +- ui/src/pages/app-detail.tsx | 18 ++++++------- ui/src/pages/apps.tsx | 16 ++++++------ ui/src/pages/dashboard.tsx | 6 ++--- ui/src/pages/edit.tsx | 6 ++--- ui/src/pages/launch.tsx | 2 +- ui/src/pages/metrics.tsx | 4 +-- ui/tsconfig.json | 18 ++++++++++--- 39 files changed, 94 insertions(+), 82 deletions(-) rename ui/src/{ui => components}/avatar.tsx (100%) rename ui/src/{ => components}/ui/alert.tsx (100%) rename ui/src/{ => components}/ui/badge.tsx (100%) rename ui/src/{ => components}/ui/button.tsx (99%) rename ui/src/{ => components}/ui/card.tsx (100%) rename ui/src/{ => components}/ui/checkbox.tsx (100%) rename ui/src/{ => components}/ui/code-block.tsx (99%) rename ui/src/{ => components}/ui/dialog.tsx (99%) rename ui/src/{ => components}/ui/dropdown-menu.tsx (99%) rename ui/src/{ => components}/ui/field.tsx (100%) rename ui/src/{ => components}/ui/input.tsx (100%) rename ui/src/{ => components}/ui/label.tsx (100%) rename ui/src/{ => components}/ui/navigation-menu.tsx (99%) rename ui/src/{ => components}/ui/radio-group.tsx (100%) rename ui/src/{ => components}/ui/select.tsx (100%) rename ui/src/{ => components}/ui/slider.tsx (100%) rename ui/src/{ => components}/ui/spinner.tsx (100%) rename ui/src/{ => components}/ui/switch.tsx (100%) rename ui/src/{ => components}/ui/table.tsx (100%) rename ui/src/{ => components}/ui/tabs.tsx (100%) rename ui/src/{ => components}/ui/textarea.tsx (100%) rename ui/src/{ => components}/ui/toast.tsx (100%) rename ui/src/{ => components}/ui/tooltip.tsx (100%) create mode 100644 ui/src/lib/clipboard.ts diff --git a/ui/components.json b/ui/components.json index 0b8c1c2..ff48a1b 100644 --- a/ui/components.json +++ b/ui/components.json @@ -15,7 +15,7 @@ "aliases": { "components": "@/components", "utils": "@/lib/utils", - "ui": "@/ui", + "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, diff --git a/ui/src/components/app-bits.tsx b/ui/src/components/app-bits.tsx index ba9b163..319e254 100644 --- a/ui/src/components/app-bits.tsx +++ b/ui/src/components/app-bits.tsx @@ -1,8 +1,8 @@ import { CheckCircle2, CircleDashed, Loader2, PauseCircle, XCircle } from 'lucide-react'; import { type ReactNode, useState } from 'react'; -import { Badge } from '@/ui/badge'; -import { Button } from '@/ui/button'; -import { Card, CardContent } from '@/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; import { Dialog, DialogClose, @@ -11,9 +11,9 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from '@/ui/dialog'; -import { Input } from '@/ui/input'; -import { Label } from '@/ui/label'; +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; export function PhaseBadge({ phase }: { phase: string }) { diff --git a/ui/src/components/app-form.tsx b/ui/src/components/app-form.tsx index 582cbf0..b6721aa 100644 --- a/ui/src/components/app-form.tsx +++ b/ui/src/components/app-form.tsx @@ -1,13 +1,13 @@ import { Plus, Trash2, UploadCloud } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { App, AppCreate, AppPatch, AppSource, EnvVar } from '@/lib/types'; -import { Button } from '@/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/ui/card'; -import { Input } from '@/ui/input'; -import { Label } from '@/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/ui/select'; -import { Switch } from '@/ui/switch'; -import { Tabs, TabsList, TabsPanel, TabsTab } from '@/ui/tabs'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { Tabs, TabsList, TabsPanel, TabsTab } from '@/components/ui/tabs'; const SUBDOMAIN_RE = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/; diff --git a/ui/src/ui/avatar.tsx b/ui/src/components/avatar.tsx similarity index 100% rename from ui/src/ui/avatar.tsx rename to ui/src/components/avatar.tsx diff --git a/ui/src/components/header.tsx b/ui/src/components/header.tsx index 33af988..ee239e4 100644 --- a/ui/src/components/header.tsx +++ b/ui/src/components/header.tsx @@ -16,7 +16,7 @@ import nebariLogoDark from '@/assets/nebari-logo_dark.svg'; import { isThemeMode, type ThemeMode } from '@/hooks/use-theme-preference'; import { getBranding } from '@/lib/branding'; import { cn } from '@/lib/utils'; -import { Avatar, AvatarFallback } from '@/ui/avatar'; +import { Avatar, AvatarFallback } from '@/components/avatar'; import { DropdownMenu, DropdownMenuContent, @@ -24,14 +24,14 @@ import { DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuTrigger, -} from '@/ui/dropdown-menu'; +} from '@/components/ui/dropdown-menu'; import { MenuBarActions, MenuBarBrand, MenuBarNav, NavigationMenu, NavLink, -} from '@/ui/navigation-menu'; +} from '@/components/ui/navigation-menu'; const NAV = [ { to: '/', label: 'Dashboard', icon: LayoutDashboard, end: true }, diff --git a/ui/src/components/layout.tsx b/ui/src/components/layout.tsx index 1f8a7d8..f158322 100644 --- a/ui/src/components/layout.tsx +++ b/ui/src/components/layout.tsx @@ -4,7 +4,7 @@ import { Header } from '@/components/header'; import { useTheme } from '@/hooks/theme-provider'; import { getUser, logout } from '@/lib/auth'; import { getBranding } from '@/lib/branding'; -import { TooltipProvider } from '@/ui/tooltip'; +import { TooltipProvider } from '@/components/ui/tooltip'; export function Layout() { const user = getUser(); diff --git a/ui/src/components/onboarding.tsx b/ui/src/components/onboarding.tsx index 2566d82..5c0c273 100644 --- a/ui/src/components/onboarding.tsx +++ b/ui/src/components/onboarding.tsx @@ -1,10 +1,10 @@ import { BookOpen, Copy, Rocket, Sparkles } from 'lucide-react'; import { type ReactNode, useState } from 'react'; import { Link } from 'react-router-dom'; -import { Button } from '@/ui/button'; -import { Card, CardContent } from '@/ui/card'; -import { toast } from '@/ui/toast'; -import { copyText } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { toast } from '@/components/ui/toast'; +import { copyText } from '@/lib/clipboard'; /** First-run guidance shown when no apps exist yet. */ export function Onboarding() { diff --git a/ui/src/ui/alert.tsx b/ui/src/components/ui/alert.tsx similarity index 100% rename from ui/src/ui/alert.tsx rename to ui/src/components/ui/alert.tsx diff --git a/ui/src/ui/badge.tsx b/ui/src/components/ui/badge.tsx similarity index 100% rename from ui/src/ui/badge.tsx rename to ui/src/components/ui/badge.tsx diff --git a/ui/src/ui/button.tsx b/ui/src/components/ui/button.tsx similarity index 99% rename from ui/src/ui/button.tsx rename to ui/src/components/ui/button.tsx index c5d7c11..9eb213a 100644 --- a/ui/src/ui/button.tsx +++ b/ui/src/components/ui/button.tsx @@ -2,7 +2,7 @@ import { useRender } from '@base-ui/react/use-render'; import { cva, type VariantProps } from 'class-variance-authority'; import { Children, isValidElement, type ReactNode } from 'react'; import { cn } from '@/lib/utils'; -import { Spinner } from '@/ui/spinner'; +import { Spinner } from '@/components/ui/spinner'; const buttonVariants = cva( "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium underline-offset-4 outline-none motion-safe:transition-[color,background-color,border-color,opacity,transform] motion-safe:duration-[--duration-fast] motion-safe:ease-[--ease-standard] motion-safe:active:scale-[0.97] hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground data-[disabled]:no-underline data-[disabled]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", diff --git a/ui/src/ui/card.tsx b/ui/src/components/ui/card.tsx similarity index 100% rename from ui/src/ui/card.tsx rename to ui/src/components/ui/card.tsx diff --git a/ui/src/ui/checkbox.tsx b/ui/src/components/ui/checkbox.tsx similarity index 100% rename from ui/src/ui/checkbox.tsx rename to ui/src/components/ui/checkbox.tsx diff --git a/ui/src/ui/code-block.tsx b/ui/src/components/ui/code-block.tsx similarity index 99% rename from ui/src/ui/code-block.tsx rename to ui/src/components/ui/code-block.tsx index 4095a1f..fdd14d6 100644 --- a/ui/src/ui/code-block.tsx +++ b/ui/src/components/ui/code-block.tsx @@ -8,7 +8,8 @@ import { useContext, useState, } from 'react'; -import { cn, copyText } from '@/lib/utils'; +import { cn } from '@/lib/utils'; +import { copyText } from '@/lib/clipboard'; interface CodeBlockContextValue { /** The raw snippet, shared so descendants don't re-thread the text. */ diff --git a/ui/src/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx similarity index 99% rename from ui/src/ui/dialog.tsx rename to ui/src/components/ui/dialog.tsx index 8690216..29cd065 100644 --- a/ui/src/ui/dialog.tsx +++ b/ui/src/components/ui/dialog.tsx @@ -2,7 +2,7 @@ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { XIcon } from 'lucide-react'; import type * as React from 'react'; import { cn } from '@/lib/utils'; -import { Button } from '@/ui/button'; +import { Button } from '@/components/ui/button'; type DialogProps = DialogPrimitive.Root.Props; diff --git a/ui/src/ui/dropdown-menu.tsx b/ui/src/components/ui/dropdown-menu.tsx similarity index 99% rename from ui/src/ui/dropdown-menu.tsx rename to ui/src/components/ui/dropdown-menu.tsx index 0614d8e..8ca0abc 100644 --- a/ui/src/ui/dropdown-menu.tsx +++ b/ui/src/components/ui/dropdown-menu.tsx @@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { CheckIcon, ChevronRightIcon, ChevronsUpDownIcon } from 'lucide-react'; import type * as React from 'react'; import { cn } from '@/lib/utils'; -import { Button, type ButtonProps } from '@/ui/button'; +import { Button, type ButtonProps } from '@/components/ui/button'; const dropdownMenuItemVariants = cva( 'relative flex w-full cursor-default items-center gap-2 rounded-[calc(var(--radius-md)-var(--spacing))] px-1.5 py-1 text-sm outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring motion-safe:transition-[color,background-color] motion-safe:duration-[--duration-fast] motion-safe:ease-[--ease-standard]', diff --git a/ui/src/ui/field.tsx b/ui/src/components/ui/field.tsx similarity index 100% rename from ui/src/ui/field.tsx rename to ui/src/components/ui/field.tsx diff --git a/ui/src/ui/input.tsx b/ui/src/components/ui/input.tsx similarity index 100% rename from ui/src/ui/input.tsx rename to ui/src/components/ui/input.tsx diff --git a/ui/src/ui/label.tsx b/ui/src/components/ui/label.tsx similarity index 100% rename from ui/src/ui/label.tsx rename to ui/src/components/ui/label.tsx diff --git a/ui/src/ui/navigation-menu.tsx b/ui/src/components/ui/navigation-menu.tsx similarity index 99% rename from ui/src/ui/navigation-menu.tsx rename to ui/src/components/ui/navigation-menu.tsx index 067510d..ca2cd72 100644 --- a/ui/src/ui/navigation-menu.tsx +++ b/ui/src/components/ui/navigation-menu.tsx @@ -10,7 +10,7 @@ import { type DropdownMenuProps, DropdownMenuTrigger, type DropdownMenuTriggerProps, -} from '@/ui/dropdown-menu'; +} from '@/components/ui/dropdown-menu'; type NavLinkProps = useRender.ComponentProps<'a'> & { /** Marks the link as the current page or section. */ diff --git a/ui/src/ui/radio-group.tsx b/ui/src/components/ui/radio-group.tsx similarity index 100% rename from ui/src/ui/radio-group.tsx rename to ui/src/components/ui/radio-group.tsx diff --git a/ui/src/ui/select.tsx b/ui/src/components/ui/select.tsx similarity index 100% rename from ui/src/ui/select.tsx rename to ui/src/components/ui/select.tsx diff --git a/ui/src/ui/slider.tsx b/ui/src/components/ui/slider.tsx similarity index 100% rename from ui/src/ui/slider.tsx rename to ui/src/components/ui/slider.tsx diff --git a/ui/src/ui/spinner.tsx b/ui/src/components/ui/spinner.tsx similarity index 100% rename from ui/src/ui/spinner.tsx rename to ui/src/components/ui/spinner.tsx diff --git a/ui/src/ui/switch.tsx b/ui/src/components/ui/switch.tsx similarity index 100% rename from ui/src/ui/switch.tsx rename to ui/src/components/ui/switch.tsx diff --git a/ui/src/ui/table.tsx b/ui/src/components/ui/table.tsx similarity index 100% rename from ui/src/ui/table.tsx rename to ui/src/components/ui/table.tsx diff --git a/ui/src/ui/tabs.tsx b/ui/src/components/ui/tabs.tsx similarity index 100% rename from ui/src/ui/tabs.tsx rename to ui/src/components/ui/tabs.tsx diff --git a/ui/src/ui/textarea.tsx b/ui/src/components/ui/textarea.tsx similarity index 100% rename from ui/src/ui/textarea.tsx rename to ui/src/components/ui/textarea.tsx diff --git a/ui/src/ui/toast.tsx b/ui/src/components/ui/toast.tsx similarity index 100% rename from ui/src/ui/toast.tsx rename to ui/src/components/ui/toast.tsx diff --git a/ui/src/ui/tooltip.tsx b/ui/src/components/ui/tooltip.tsx similarity index 100% rename from ui/src/ui/tooltip.tsx rename to ui/src/components/ui/tooltip.tsx diff --git a/ui/src/lib/clipboard.ts b/ui/src/lib/clipboard.ts new file mode 100644 index 0000000..19e9c9b --- /dev/null +++ b/ui/src/lib/clipboard.ts @@ -0,0 +1,24 @@ +/** + * Copy text to the clipboard. `navigator.clipboard` only exists in secure + * contexts (HTTPS / localhost); on plain-HTTP deployments (e.g. local dev + * with tls.enabled=false) fall back to the legacy hidden-textarea approach. + * Rejects when neither path succeeds. + */ +export async function copyText(text: string): Promise { + if (navigator.clipboard) { + await navigator.clipboard.writeText(text); + return; + } + const ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + try { + if (!document.execCommand('copy')) throw new Error('copy command rejected'); + } finally { + ta.remove(); + } +} diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 3741c1c..9ad0df4 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -4,28 +4,3 @@ import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } - -/** - * Copy text to the clipboard. `navigator.clipboard` only exists in secure - * contexts (HTTPS / localhost); on plain-HTTP deployments (e.g. local dev - * with tls.enabled=false) fall back to the legacy hidden-textarea approach. - * Rejects when neither path succeeds. - */ -export async function copyText(text: string): Promise { - if (navigator.clipboard) { - await navigator.clipboard.writeText(text); - return; - } - const ta = document.createElement('textarea'); - ta.value = text; - ta.setAttribute('readonly', ''); - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - document.body.appendChild(ta); - ta.select(); - try { - if (!document.execCommand('copy')) throw new Error('copy command rejected'); - } finally { - ta.remove(); - } -} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 04d7b12..2fd4051 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -12,7 +12,7 @@ import { DashboardPage } from '@/pages/dashboard'; import { EditPage } from '@/pages/edit'; import { LaunchPage } from '@/pages/launch'; import { MetricsPage } from '@/pages/metrics'; -import { Toaster } from '@/ui/toast'; +import { Toaster } from '@/components/ui/toast'; import '@/index.css'; // Branding comes from /config.json (ui.branding.* in the chart). Start the diff --git a/ui/src/pages/app-detail.tsx b/ui/src/pages/app-detail.tsx index 584dcfc..b15ab46 100644 --- a/ui/src/pages/app-detail.tsx +++ b/ui/src/pages/app-detail.tsx @@ -17,15 +17,15 @@ import { Link, useNavigate, useParams } from 'react-router-dom'; import { AppThumbnail, ConfirmDeleteDialog, KindBadge, metricValue, PhaseBadge, Sparkline, SourceBadge } from '@/components/app-bits'; import { api } from '@/lib/api'; import type { App } from '@/lib/types'; -import { Alert, AlertDescription, AlertTitle } from '@/ui/alert'; -import { Button } from '@/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/ui/card'; -import { CodeBlock, CodeBlockBody } from '@/ui/code-block'; -import { Input } from '@/ui/input'; -import { Spinner } from '@/ui/spinner'; -import { Switch } from '@/ui/switch'; -import { Tabs, TabsList, TabsPanel, TabsTab } from '@/ui/tabs'; -import { toast } from '@/ui/toast'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { CodeBlock, CodeBlockBody } from '@/components/ui/code-block'; +import { Input } from '@/components/ui/input'; +import { Spinner } from '@/components/ui/spinner'; +import { Switch } from '@/components/ui/switch'; +import { Tabs, TabsList, TabsPanel, TabsTab } from '@/components/ui/tabs'; +import { toast } from '@/components/ui/toast'; const MAX_SAMPLES = 30; const errMessage = (err: unknown) => (err instanceof Error ? err.message : String(err)); diff --git a/ui/src/pages/apps.tsx b/ui/src/pages/apps.tsx index 1c6f753..123b5fe 100644 --- a/ui/src/pages/apps.tsx +++ b/ui/src/pages/apps.tsx @@ -18,8 +18,8 @@ import { AppThumbnail, ConfirmDeleteDialog, PhaseBadge, SourceBadge } from '@/co import { Onboarding } from '@/components/onboarding'; import { api } from '@/lib/api'; import type { App } from '@/lib/types'; -import { Button } from '@/ui/button'; -import { Checkbox } from '@/ui/checkbox'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogClose, @@ -28,12 +28,12 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from '@/ui/dialog'; -import { Input } from '@/ui/input'; -import { Spinner } from '@/ui/spinner'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/table'; -import { toast } from '@/ui/toast'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/ui/tooltip'; +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Spinner } from '@/components/ui/spinner'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { toast } from '@/components/ui/toast'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; const appLabel = (app: App) => app.displayName || app.name; diff --git a/ui/src/pages/dashboard.tsx b/ui/src/pages/dashboard.tsx index 2544118..d5eacfd 100644 --- a/ui/src/pages/dashboard.tsx +++ b/ui/src/pages/dashboard.tsx @@ -4,9 +4,9 @@ import { Link } from 'react-router-dom'; import { AppThumbnail, BarList, KindBadge, PhaseBadge, SourceBadge, StatCard } from '@/components/app-bits'; import { Onboarding } from '@/components/onboarding'; import { api } from '@/lib/api'; -import { Button } from '@/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/ui/card'; -import { Spinner } from '@/ui/spinner'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Spinner } from '@/components/ui/spinner'; export function DashboardPage() { const analytics = useQuery({ queryKey: ['analytics'], queryFn: api.analytics }); diff --git a/ui/src/pages/edit.tsx b/ui/src/pages/edit.tsx index 0c21309..0e0db86 100644 --- a/ui/src/pages/edit.tsx +++ b/ui/src/pages/edit.tsx @@ -4,9 +4,9 @@ import { Link, useNavigate, useParams } from 'react-router-dom'; import { AppForm, type SubmitPayload } from '@/components/app-form'; import { api } from '@/lib/api'; import { getConfig } from '@/lib/auth'; -import { Alert, AlertDescription, AlertTitle } from '@/ui/alert'; -import { Spinner } from '@/ui/spinner'; -import { toast } from '@/ui/toast'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Spinner } from '@/components/ui/spinner'; +import { toast } from '@/components/ui/toast'; export function EditPage() { const { namespace = '', name = '' } = useParams(); diff --git a/ui/src/pages/launch.tsx b/ui/src/pages/launch.tsx index 662ec68..3f4a7c7 100644 --- a/ui/src/pages/launch.tsx +++ b/ui/src/pages/launch.tsx @@ -4,7 +4,7 @@ import { AppForm, type SubmitPayload } from '@/components/app-form'; import { api } from '@/lib/api'; import { getConfig } from '@/lib/auth'; import type { AppCreate } from '@/lib/types'; -import { toast } from '@/ui/toast'; +import { toast } from '@/components/ui/toast'; export function LaunchPage() { const navigate = useNavigate(); diff --git a/ui/src/pages/metrics.tsx b/ui/src/pages/metrics.tsx index fccf130..50cc0d4 100644 --- a/ui/src/pages/metrics.tsx +++ b/ui/src/pages/metrics.tsx @@ -4,8 +4,8 @@ import type { ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { api } from '@/lib/api'; import type { App } from '@/lib/types'; -import { Card, CardContent, CardHeader, CardTitle } from '@/ui/card'; -import { Spinner } from '@/ui/spinner'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Spinner } from '@/components/ui/spinner'; export function MetricsPage() { const metrics = useQuery({ diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 1ffef60..50f795e 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,7 +1,19 @@ { "files": [], "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + } + } } From 5fa467689a0457231dde866b2d21af9b7ed5eee2 Mon Sep 17 00:00:00 2001 From: Johnny Bouder Date: Fri, 4 Sep 2026 12:31:46 -0400 Subject: [PATCH 2/3] chore(ui): re-sync @nebari registry components Re-add every installed registry item with `shadcn add --overwrite` so the files match the published @nebari registry: alert, badge, button, card, checkbox, code-block, dialog, dropdown-menu, field, input, label, navigation-menu, radio-group, select, slider, spinner, switch, table, tabs, textarea, toast, tooltip, utils and use-theme-preference. Install the @nebari/claude-skill so .claude/skills/nebari-ui/SKILL.md ships with the frontend. The registry toast no longer wraps the Base UI manager in success()/error() helpers; it exports the manager itself, with priority derived from the toast type. Update every caller to `toast.add({ type, title, description })`. Co-Authored-By: Claude Fable 5.1 --- ui/.claude/skills/nebari-ui/SKILL.md | 655 +++++++++++++++++++++++ ui/src/components/onboarding.tsx | 2 +- ui/src/components/ui/button.tsx | 2 +- ui/src/components/ui/code-block.tsx | 25 +- ui/src/components/ui/dialog.tsx | 4 +- ui/src/components/ui/dropdown-menu.tsx | 4 +- ui/src/components/ui/input.tsx | 2 +- ui/src/components/ui/navigation-menu.tsx | 2 +- ui/src/components/ui/select.tsx | 20 +- ui/src/components/ui/slider.tsx | 4 +- ui/src/components/ui/switch.tsx | 4 +- ui/src/components/ui/table.tsx | 71 ++- ui/src/components/ui/tabs.tsx | 4 +- ui/src/components/ui/textarea.tsx | 2 +- ui/src/components/ui/toast.tsx | 640 +++++++++++++++++++--- ui/src/components/ui/tooltip.tsx | 2 +- ui/src/pages/app-detail.tsx | 16 +- ui/src/pages/apps.tsx | 16 +- ui/src/pages/edit.tsx | 4 +- ui/src/pages/launch.tsx | 11 +- 20 files changed, 1357 insertions(+), 133 deletions(-) create mode 100644 ui/.claude/skills/nebari-ui/SKILL.md diff --git a/ui/.claude/skills/nebari-ui/SKILL.md b/ui/.claude/skills/nebari-ui/SKILL.md new file mode 100644 index 0000000..6f64df3 --- /dev/null +++ b/ui/.claude/skills/nebari-ui/SKILL.md @@ -0,0 +1,655 @@ +--- +name: nebari-ui +description: >- + Add and use components from the Nebari design system (the @nebari shadcn + registry) in this app. Use when asked to "add a nebari component", "use the + nebari button/badge/alert/spinner/skeleton", "install the nebari theme", "animate, + add a transition, make it feel polished, or add an entrance animation with + nebari", "build with nebari components", or "build/update the app + header (top bar, navigation bar, profile menu, notifications menu, theme + picker) to match nebari". Covers registry setup (the @nebari namespace in + components.json + the shadcn add command), the component catalog (variants, + sizes, props), the Base UI render-prop composition convention, theming (the + @nebari/theme tokens, CSS variables, and light/dark), the canonical app + header recipe (MenuBar composition, header tokens, notifications and profile + menus, the menuitemradio theme picker), and motion (duration/easing tokens, + entrance animations, overlay transitions, accessibility guardrails). +--- + +# Using the Nebari design system + +[Nebari design](https://github.com/nebari-dev/nebari-design) is a +[shadcn component registry](https://ui.shadcn.com/docs/registry) styled with the +Nebari brand. You install its components into this app with the `shadcn` CLI — +they're copied into your codebase, but Nebari treats them as **upstream-managed +source**: you extend them at the call site rather than editing the installed +files (see [Treat installed components as managed](#treat-installed-components-as-managed)). +This skill covers setup, the catalog, the composition convention, theming, and +the canonical app-header recipe. + +## Step 1 — register the `@nebari` namespace (once) + +Add the `@nebari` registry to the consumer's `components.json` so +`shadcn add @nebari/` resolves. The items are served as JSON from the +project's GitHub Pages site: + +```json +{ + "$schema": "https://ui.shadcn.com/schema.json", + "registries": { + "@nebari": "https://nebari-dev.github.io/nebari-design/r/{name}.json" + } +} +``` + +`{name}` is the placeholder shadcn substitutes per item. This block sits +alongside the app's existing `style` / `tailwind` / `aliases` config — it does +not replace them. A standard shadcn-initialized project (run `npx shadcn init` +first if `components.json` doesn't exist yet) is the only prerequisite. + +## Step 2 — install components + +```sh +npx shadcn add @nebari/ +``` + +Most components depend on the shared `cn()` helper (the `utils` item) and the +theme tokens, and shadcn pulls those `registryDependencies` in automatically — +you don't list them yourself. Install the theme explicitly the first time (see +[Theming](#theming)): + +```sh +npx shadcn add @nebari/theme +npx shadcn add @nebari/button +``` + +Installed files land under the app's configured aliases (`@/ui`, `@/hooks`, +`@/lib`), so +imports look like `import { Button } from '@/components/ui/button'` — match the +host app's existing alias resolution. + +## Treat installed components as managed + +`shadcn add` copies the source into your repo, but treat the installed `ui/*` +and `lib/*` files as **upstream-managed, not app-owned**. Don't edit them. + +- **Why** — these files are regenerated by `shadcn add` on every upgrade, so any + local edit is silently overwritten and lost. Editing also forks you away from + the shared design system: the next consumer of the same component gets + different behavior than yours, which is exactly what a design system exists to + prevent. +- **To change look or behavior, do it at the call site, never in the file:** + - Pass extra classes via `className` — they're merged with `cn()`, so your + classes win without touching the source. + - Swap the rendered element with the Base UI `render` prop (link button, etc.) + instead of rewrapping or editing. + - Build a thin **wrapper component** in your own app that composes the Nebari + component when you need app-specific defaults or behavior. + - Put custom styling and motion in your own `globals.css` or a CSS module — the + only files you edit are ones you own. +- **If a component genuinely can't express what you need** through the above, + don't fork it locally — request the change upstream in + [nebari-design](https://github.com/nebari-dev/nebari-design) so every app gets + it and stays consistent. Until then, wrap rather than edit. + +## Discovering what's available + +The registry is the source of truth for the catalog — don't rely on a +hard-coded list here, which would drift as components are added. To see what +exists and learn a component's exact API: + +- **List every item** — fetch the registry index, which names and describes each + installable item (components, the `utils` helper, the `theme`, this skill): + + ```sh + curl -s https://nebari-dev.github.io/nebari-design/r/registry.json + ``` + +- **Inspect one before installing** — `shadcn view` prints an item's + description, dependencies, and its full source: + + ```sh + npx shadcn view @nebari/button + ``` + +- **After installing, read the source** — components are copied into your repo + (treat them as managed — see + [Treat installed components as managed](#treat-installed-components-as-managed)). + The exact `variant`/`size` names and props live in the + component's `cva` block and its props type; open the installed `.tsx` (e.g. + `@/components/ui/button.tsx`) — that file, not any doc, is authoritative. + +## How Nebari components are built + +Every component follows the same shape, so once you've seen one you can use any: + +- Styled with **semantic theme tokens** (`bg-primary`, `text-muted-foreground`, + …), so it follows light/dark automatically — never restyle with raw hex or + `dark:` variants. +- Sets stable `data-slot` / `data-variant` / `data-size` attributes you can + target in CSS or tests. +- Exports its `cva` class function (`buttonVariants`, `badgeVariants`, …) + alongside the component for reuse. +- `variant` / `size` (where present) select the look; read the source for the + exact set a given component offers. + +`Button` is a representative example — variants, sizes, a `loading` state, and +`render`-prop composition: + +```tsx +import { Button } from '@/components/ui/button'; + + + + + +``` + +Composed components (e.g. `Alert` with `AlertTitle` / `AlertDescription` / +`AlertAction`) export their parts as named exports from the same module — the +installed source and `shadcn view` show how the pieces fit together. + +## Composition (Base UI `render` prop) + +Polymorphic components (`Button`, `Badge`) use **Base UI's `render` prop** to +change the rendered element while keeping their styling — this is Nebari's +equivalent of Radix's `asChild`. Pass an element and the component merges its +classes, `data-*` attributes, and props onto it: + +```tsx +// Render a Button as a link + + +// Render a Badge as a link +}>new +``` + +The component's `data-slot` and styling are preserved on the swapped element, so +a `; +} +``` + +- **Storage key:** defaults to `nebari:themeMode`. An app that already + persists a preference must pass its existing key so users keep it: + `` (or + `useThemePreference({ storageKey: '…' })`). + +- **Flash prevention:** the saved theme is applied from a React effect, so a + dark-preference user sees a light flash while the bundle loads — unless the + class is set pre-paint. `themeBootstrapScript(storageKey?)` is the single + source of truth for that snippet: it resolves `dark` exactly like the hook, + from the same storage key and default. Paste its output into a ` + ``` + + If you use a non-default storage key, regenerate the snippet with + `themeBootstrapScript('your:key')` instead of editing it by hand. + +## App header + +Nebari apps in one deployment share a single header, so users read them as one +product. Don't re-derive the layout from another app's source — this section is +the canonical recipe. The reference implementation is +[nebari-landing's `Header.tsx`](https://github.com/nebari-dev/nebari-landing/blob/main/frontend/src/components/Header.tsx). + +### Composition and sizing + +The header is built from `@nebari/navigation-menu` (its menus also need +`@nebari/dropdown-menu`, `@nebari/avatar`, and `@nebari/button`): + +```sh +npx shadcn add @nebari/navigation-menu @nebari/dropdown-menu @nebari/avatar @nebari/button +``` + +- **`NavigationMenu`** (alias of `MenuBar`, a semantic `
`) is the bar. + Its default is `h-12 px-3`; the canonical app header overrides to **`h-14`** + with **`pl-4`** (keeping the default `pr-3`) and the header tokens: + + ```tsx + + ``` + +- **`MenuBarBrand`** holds the logo, left-aligned, linking home: + + ```tsx + + Nebari + + ``` + + The logo is **`h-8 w-auto`**. Apps with light/dark logo variants pick by + `isDarkMode` from [`useTheme`](#dark-mode-state-usethemepreference). +- **`MenuBarNav`** (optional) holds center `NavLink` / `NavDropdownMenu` items. +- **`MenuBarActions`** is the right-side slot (use `className="gap-2"`) holding, + in order: the notifications menu (if the app has notifications) and the + profile/account menu. + +### Header token contract + +The header is styled **only** with semantic tokens — no hardcoded colors. +The chrome surface itself ships with `@nebari/theme`: **`--header`** / +**`--header-foreground`** are the app-chrome layer of the surface stack +(`--canvas` → `--header` → `--card`/`--popover` → `--muted`), so use +`bg-header text-header-foreground` with the plain `border-border`. Only the +interaction and status colors below are **app-defined** — declare them in your +theme CSS in both light and dark, plus the Tailwind `@theme` mappings that make +`bg-header-action-hover` etc. resolve: + +```css +:root { + --header-action-hover: #d9d9dc; + --notification-badge: #d00000; + --sign-out-foreground: #d2161c; +} + +.dark { + --header-action-hover: #4a4a50; + --notification-badge: #e00000; + --sign-out-foreground: #ff6b6b; +} + +@theme inline { + --color-header-action-hover: var(--header-action-hover); + --color-notification-badge: var(--notification-badge); + --color-sign-out-foreground: var(--sign-out-foreground); +} +``` + +Registry-provided vs. app-defined: + +| Token | Source | +|---|---| +| `--primary`, `--muted`, `--card`, `--border`, … | `@nebari/theme` (registry) | +| `--canvas`, `--header`, `--header-foreground` | `@nebari/theme` (registry) | +| `--header-action-hover`, `--notification-badge`, `--sign-out-foreground` | **app-defined** (values above) | + +### Notifications menu + +A ghost icon `DropdownMenuTrigger` with a `Bell`, an unread-count badge on +`bg-notification-badge`, and a **552px** menu. `modal={false}` keeps the page +scrollable while the menu is open — don't omit it: + +```tsx + open && markViewed()}> + + + {unreadCount > 0 && ( + + {unreadCount} + + )} + + + + {/* one DropdownMenuItem per notification; unread items get a + h-2 w-2 rounded-full bg-primary dot */} + + + +``` + +Only render the bell if the app actually has notifications — don't ship an +empty menu. + +### Profile / account menu + +Avatar + user name + `ChevronDown` as the trigger; a **248px** menu +(`w-[248px] p-2`) with a name/email section, the theme picker, then a red +sign-out item: + +```tsx + + + + {user.image && } + + {initials} + + + {user.name} + + + + +
+

{user.name}

+

{user.email}

+
+ + {/* Theme picker — see below */} + + + + + +
+
+
+``` + +When no user is signed in, render a plain ` +``` + +#### Overlay enter/exit — Base UI `data-starting-style` / `data-ending-style` + +Base UI popups (Dialog, Select, Tooltip, …) apply `data-starting-style` and +`data-ending-style` attributes during CSS transitions so you can define enter +and exit animations purely in CSS. Use a plain `