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 `