npm package: @metaphor-cloud/ui
Description: Shared React component library for Metaphor projects. Themeable, accessible, built on Radix UI primitives.
Latest version: 0.4.0

# @metaphor-cloud/ui

Shared React component library for Metaphor projects. Themeable, accessible, built on Radix UI primitives.

Requires **React 19+** and **Tailwind CSS 4** (optional, for extending with the preset).

## Quick Start

```bash
npm install @metaphor-cloud/ui
```

```tsx
import "@metaphor-cloud/ui/metaphor"; // theme + styles in one import
import { ThemeProvider, Button } from "@metaphor-cloud/ui";

function App() {
  return (
    <ThemeProvider>
      <Button variant="primary">Click me</Button>
    </ThemeProvider>
  );
}
```

That's it. `ThemeProvider` reads the theme's default mode automatically from the CSS (`--theme-default`), persists the user's choice to localStorage, and syncs across tabs.

## Themes

Two themes ship out of the box:

| Theme | Import | Default Mode | Accent | Fonts |
|-------|--------|--------------|--------|-------|
| Metaphor | `@metaphor-cloud/ui/metaphor` | Dark | Blue | Plus Jakarta Sans, Outfit, JetBrains Mono |
| Um | `@metaphor-cloud/ui/um` | Light | Ocean blue | Nunito, JetBrains Mono |

### Switching themes

Replace the CSS import — no other changes needed:

```tsx
import "@metaphor-cloud/ui/um"; // swap to Um theme
```

### Dark / light mode

`ThemeProvider` handles mode switching. Use `useThemeContext()` anywhere inside the provider:

```tsx
import { useThemeContext } from "@metaphor-cloud/ui";

function ThemeToggle() {
  const { theme, resolved, cycleTheme, setTheme } = useThemeContext();

  return (
    <button onClick={cycleTheme}>
      {resolved === "dark" ? "🌙" : "☀️"}
      {theme === "system" && " (auto)"}
    </button>
  );
}
```

Modes cycle: **dark → light → system → dark**. The `system` mode follows the OS preference.

### Creating a custom theme

Copy `src/themes/metaphor.css` and override the CSS variables:

```css
:root,
[data-theme="dark"] {
  --theme-default: dark;
  --accent: #your-color;
  --bg: #your-bg;
  /* ... */
}

[data-theme="light"] {
  --accent: #your-light-color;
  --bg: #your-light-bg;
  /* ... */
}
```

## Tailwind CSS Preset

If your app uses Tailwind CSS 4, extend from the preset to get all the theme-aware utility classes (colors, fonts, animations, border radii):

```css
/* your app's main CSS */
@import "tailwindcss";
@import "@metaphor-cloud/ui/metaphor";

@config "./tailwind.config.ts";
```

```ts
// tailwind.config.ts
import preset from "@metaphor-cloud/ui/tailwind-preset";
import type { Config } from "tailwindcss";

export default {
  presets: [preset],
  content: ["./src/**/*.{ts,tsx}"],
} satisfies Config;
```

The preset gives you:

- **Colors**: `bg-bg-elevated`, `text-accent`, `border-border`, `text-success`, etc. — all CSS variable-backed, all theme-reactive
- **Fonts**: `font-sans`, `font-display`, `font-mono`
- **Animations**: `animate-fade-in`, `animate-fade-up`, `animate-accordion-down`, etc.
- **Spacing**: `section-gap`
- **Border radius**: `rounded` / `rounded-lg` / `rounded-sm` — derived from the theme's `--radius` variable

If your app uses `data-theme` for dark mode (like this library does), add this to your CSS:

```css
@custom-variant dark (&:where([data-theme="dark"]), &:where([data-theme="dark"] *));
```

## Components

All components accept `ref` as a regular prop (React 19), support `className` for overrides via Tailwind, and are fully typed.

### Layout

| Component | Parts | Usage |
|-----------|-------|-------|
| `Card` | `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Content container |
| `Separator` | — | Horizontal or vertical divider |
| `Table` | `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell`, `TableCaption` | Data table |
| `Skeleton` | — | Loading placeholder |

### Forms

| Component | Usage |
|-----------|-------|
| `Button` | `variant`: primary, secondary, danger, ghost, link. `size`: sm, md, lg, icon. Supports `asChild` for custom elements. |
| `Input` | Text input |
| `Textarea` | Multi-line input |
| `Label` | Form label |
| `Select` | `SelectTrigger`, `SelectContent`, `SelectItem`, `SelectGroup`, `SelectValue`, `SelectLabel`, `SelectSeparator` |
| `Checkbox` | Checkable input |
| `Switch` | Toggle switch |

### Feedback

| Component | Usage |
|-----------|-------|
| `Alert` | `variant`: default, info, success, danger, warning. With `AlertTitle`, `AlertDescription`. |
| `Badge` | `variant`: default, success, danger, warning, outline |
| `Progress` | Progress bar with value/max |
| `Toast` | See [Toast notifications](#toast-notifications) below |

### Overlays

| Component | Parts | Usage |
|-----------|-------|-------|
| `Dialog` | `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter`, `DialogTitle`, `DialogDescription`, `DialogClose` | Modal dialog. `DialogContent` accepts `hideCloseButton`. |
| `AlertDialog` | `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogFooter`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel` | Confirmation dialog |
| `DropdownMenu` | `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSeparator`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuSub`, `DropdownMenuSubTrigger`, `DropdownMenuSubContent` | Context/action menu |
| `Tooltip` | `TooltipProvider`, `TooltipTrigger`, `TooltipContent` | Hover tooltip |
| `Popover` | `PopoverTrigger`, `PopoverContent` | Click popover |

### Navigation

| Component | Parts | Usage |
|-----------|-------|-------|
| `Nav` | — | Responsive navbar with theme toggle, dropdowns, mobile drawer |
| `Breadcrumb` | — | Semantic breadcrumb nav with automatic BreadcrumbList JSON-LD |

### SEO & Social

| Component | Usage |
|-----------|-------|
| `MetaHead` | Declarative `<head>` management — title, description, Open Graph, Twitter Cards, canonical URL, robots |
| `StructuredData` | Injects JSON-LD `<script>` tags — typed props for Organization, Article, Product, BreadcrumbList, FAQPage, WebSite |

### Other

| Component | Usage |
|-----------|-------|
| `Accordion` | `AccordionItem`, `AccordionTrigger`, `AccordionContent` — collapsible sections |
| `Tabs` | `TabsList`, `TabsTrigger`, `TabsContent` — tabbed navigation |
| `CodeBlock` | `inline` prop for inline code, block by default |

## Toast Notifications

Add `<Toaster />` once in your layout, then call `toast()` from anywhere:

```tsx
import { Toaster, toast } from "@metaphor-cloud/ui";

// In your root layout
function Layout({ children }) {
  return (
    <>
      {children}
      <Toaster />
    </>
  );
}

// Anywhere in your app
function SaveButton() {
  const handleSave = async () => {
    await save();
    toast({
      title: "Saved",
      description: "Your changes have been saved.",
      variant: "success", // "default" | "success" | "danger" | "warning"
    });
  };

  return <Button onClick={handleSave}>Save</Button>;
}
```

`toast()` returns `{ id, dismiss, update }` for programmatic control. Up to 5 toasts show at once, auto-dismissing after 5 seconds (customisable via `duration`).

## SEO & Social Previews

### `<MetaHead>`

Manages `<title>`, meta description, Open Graph, Twitter Cards, canonical URL, and robots directives. All tags are automatically cleaned up on unmount or when props change.

```tsx
import { MetaHead } from "@metaphor-cloud/ui";

<MetaHead
  title="My Page"
  titleTemplate="%s | My Site"
  description="A short description for search results and social previews."
  url="https://example.com/page"
  image="https://example.com/og.png"
  imageAlt="Preview of My Page"
  imageWidth={1200}
  imageHeight={630}
  siteName="My Site"
  twitterCard="summary_large_image"
  twitterSite="@mysite"
/>
```

Props: `title`, `titleTemplate`, `description`, `url`, `image`, `imageAlt`, `imageWidth`, `imageHeight`, `type` (default `"website"`), `siteName`, `locale`, `twitterCard`, `twitterSite`, `twitterCreator`, `noindex`, `nofollow`.

### `<StructuredData>`

Injects a `<script type="application/ld+json">` tag into the document head. Typed props for common schema.org types.

```tsx
import { StructuredData } from "@metaphor-cloud/ui";

// Organization
<StructuredData
  data={{
    "@type": "Organization",
    name: "Acme Corp",
    url: "https://acme.com",
    logo: "https://acme.com/logo.png",
    sameAs: ["https://twitter.com/acme", "https://linkedin.com/company/acme"],
  }}
/>

// FAQ page
<StructuredData
  data={{
    "@type": "FAQPage",
    mainEntity: [
      {
        "@type": "Question",
        name: "What is Acme?",
        acceptedAnswer: { "@type": "Answer", text: "Acme builds widgets." },
      },
    ],
  }}
/>
```

### `<Breadcrumb>`

Semantic breadcrumb navigation that automatically emits BreadcrumbList JSON-LD structured data.

```tsx
import { Breadcrumb } from "@metaphor-cloud/ui";

<Breadcrumb
  items={[
    { label: "Home", href: "/" },
    { label: "Blog", href: "/blog" },
    { label: "My Post" },
  ]}
/>
```

Supports `renderLink` for framework routers (Next.js, React Router, etc.), a custom `separator`, and `structuredData={false}` to disable JSON-LD output.

```tsx
<Breadcrumb
  items={items}
  separator="/"
  renderLink={({ href, children, className }) => (
    <Link to={href} className={className}>{children}</Link>
  )}
/>
```

### `createOgImageUrl`

Factory for building Open Graph image URLs. Works with any OG image service (Vercel OG, custom endpoints, etc.).

```tsx
import { createOgImageUrl } from "@metaphor-cloud/ui";

const ogImage = createOgImageUrl({
  baseUrl: "https://og.mysite.com/api/image",
  defaults: { siteName: "My Site", theme: "dark" },
});

// Returns: "https://og.mysite.com/api/image?title=Hello+World&siteName=My+Site&theme=dark"
ogImage({ title: "Hello World" });

// Use with MetaHead
<MetaHead
  title="Hello World"
  image={ogImage({ title: "Hello World" })}
  imageWidth={1200}
  imageHeight={630}
/>
```

For custom URL formats, pass a `buildUrl` function:

```tsx
const ogImage = createOgImageUrl({
  baseUrl: "https://og.mysite.com",
  buildUrl: (base, params) => `${base}/${encodeURIComponent(params.title)}.png`,
});
```

## Hooks

### `useThemeContext()`

Access theme state from inside `ThemeProvider`. Returns `{ theme, resolved, setTheme, cycleTheme }`.

### `useTheme(defaultTheme?)`

Standalone theme hook (used internally by `ThemeProvider`). Use directly if you need theme management without the context provider.

### `useMobile(breakpoint?)`

Returns `true` when the viewport is at or below the breakpoint (default 768px).

```tsx
import { useMobile } from "@metaphor-cloud/ui";

const isMobile = useMobile();     // default: 768px
const isTablet = useMobile(1024); // custom breakpoint
```

### `useCanonical(url)`

Manages a `<link rel="canonical">` tag in the document head. Cleans up on unmount.

```tsx
import { useCanonical } from "@metaphor-cloud/ui";

useCanonical("https://example.com/page");
```

### `useAlternateLinks(links)`

Manages `<link rel="alternate" hreflang="…">` tags for multi-language sites.

```tsx
import { useAlternateLinks } from "@metaphor-cloud/ui";

useAlternateLinks([
  { hreflang: "en", href: "https://example.com/en/page" },
  { hreflang: "fr", href: "https://example.com/fr/page" },
  { hreflang: "x-default", href: "https://example.com/page" },
]);

## CSS Imports

| Import | Contents |
|--------|----------|
| `@metaphor-cloud/ui/metaphor` | Metaphor theme variables + base styles + utilities (all-in-one) |
| `@metaphor-cloud/ui/um` | Um theme variables + base styles + utilities (all-in-one) |
| `@metaphor-cloud/ui/style.css` | Base styles + utilities only (no theme variables) |
| `@metaphor-cloud/ui/themes/metaphor.css` | Metaphor theme variables only |
| `@metaphor-cloud/ui/themes/um.css` | Um theme variables only |

Use the all-in-one imports for simplicity. Use the split imports if you need to load theme variables separately (e.g. for dynamic theme switching at the CSS level).

## TypeScript

All components and hooks are fully typed. The library re-exports Radix primitive prop types for convenience:

```tsx
import type {
  ButtonProps,
  BadgeProps,
  InputProps,
  DialogProps,
  SelectProps,
  // ... etc
} from "@metaphor-cloud/ui";
```

## Development

```bash
npm install
npm run build    # tsc + vite build
npm run dev      # vite dev server
```