# Viewership UI: component conventions

The contract every component (and every agent porting one) follows. If it
conflicts with an upstream source, this file wins.

## 1. What a component is

One folder per component under `src/ui/registry/<slug>/`:

| File | Purpose |
| --- | --- |
| `<Name>.astro` | The component. Consumers copy this. |
| `*.ts` (optional) | Helpers the component imports. List them in `meta.files`. |
| `demo.astro` | Gallery demo. Receives `variant` when `meta.variants` is set. |
| `meta.ts` | `export default { ... } satisfies ComponentMeta` (see `src/lib/types.ts`). |

The site finds components by globbing `meta.ts`. **Never edit a shared file to
register a component.** `src/ui/registry/button/` is the reference; read it first.

## 2. Stack rules

- **Astro + TypeScript + scoped CSS only.** No React, Tailwind or CSS-in-JS.
- **No runtime npm dependencies** unless reimplementing is unreasonable. If you
  add one, check its license (MIT/ISC/Apache-2.0 only), list it in `meta.npm`,
  and say so in your report.
- **Icons are inline SVG.** Use Lucide (ISC) paths. Iconoir (MIT) is fine when
  porting a component that used it. **Never copy Central Icons (proprietary).**
- Import other registry components relatively
  (`import Button from "../button/Button.astro";`) and list their slugs in
  `meta.dependencies`.

## 3. Styling

- Read every color, radius, shadow, font and easing from the `--vs-*` tokens in
  `src/ui/styles/foundation.css`. Never hard-code a theme color (pure `white`
  text on a filled accent is fine).
- The root element gets class `vs-<slug>`. Scope everything under the
  component's `<style>`.
- Script-created DOM is not scoped. Style it with `:global(...)` under your root class.
- Keyframes: use the shared `vs-fade-in`, `vs-fade-up`, `vs-pop-in`,
  `vs-shimmer-text`, `vs-pixel-on`, `vs-spin`, `vs-caret-blink` and `vs-pulse`.
  Prefix new ones `vs-<slug>-*`.
- Check both themes (`<html data-theme="dark">`).

### Tailwind to token map (for porting Beautiful UI)

| Upstream class | Use |
| --- | --- |
| `bg-surface`, `bg-canvas`, `bg-inset`, `bg-field`, `bg-page` | `var(--vs-surface)` / `--vs-canvas` / `--vs-inset` / `--vs-field` / `--vs-page` |
| `hover:bg-hover`, `bg-hover-2` | `var(--vs-hover)` / `var(--vs-hover-2)` |
| `text-ink`, `text-ink-2`, `text-ink-3` | `var(--vs-ink)` / `--vs-ink-2` / `--vs-ink-3` |
| `border-line`, `border-line-strong`, `var(--grid-line)` | `var(--vs-line)` / `--vs-line-strong` / `--vs-grid-line` |
| `text-accent`, `bg-accent-tint`, `text-accent-ink` | `var(--vs-accent)` / `--vs-accent-tint` / `--vs-accent-ink` |
| `text-green`, `bg-green-tint` (and red, orange) | `var(--vs-green)` / `--vs-green-tint` … |
| `shadow-hairline`, `shadow-btn`, `shadow-card`, `shadow-raised`, `shadow-overlay`, `shadow-inset-field` | `var(--vs-shadow-…)` |
| `rounded-chip`, `rounded-control`, `rounded-card`, `rounded-window`, `rounded-full` | `var(--vs-radius-chip)` / `-control` / `-card` / `-window` / `-pill` |
| `font-mono` | `font-family: var(--vs-font-mono)` |
| `var(--tooltip-*)` | `var(--vs-tooltip-*)` |
| Spacing `p-3`, `gap-1.5`, `h-7`, … | `n × 4px` (`p-3` = 12px) |
| Arbitrary values `text-[12.5px]`, `h-[27px]` | Keep the exact value. |
| `cubic-bezier(0.23,1,0.32,1)` | `var(--vs-ease-out)` |

Blocks marked **COMPONENT-SPECIFIC** in `reference/beautiful-ui/app/globals.css`
belong to that component. Port them into its `<style>`.

## 4. Behaviour (React to vanilla)

- **Server-render the markup** from props. Interactivity lives in a `<script>`
  in the component; Astro bundles it once per page.
- **Every instance works independently.** Boot pattern:

  ```ts
  <script>
    function init(root: HTMLElement) {
      /* wire one instance */
    }
    const boot = () =>
      document.querySelectorAll<HTMLElement>(".vs-slug:not([data-vs-ready])").forEach((root) => {
        root.dataset.vsReady = "";
        init(root);
      });
    boot();
    document.addEventListener("astro:page-load", boot); // View Transitions support
  </script>
  ```

- Pass data to the script through `data-*` attributes, or a
  `<script type="application/json" data-vs-config>` inside the root. Never use
  `define:vars`: it disables bundling.
- **Animated demos** (streams, loaders, step sequences): expose `autoplay` and
  `loop` props. Start when the root scrolls into view (IntersectionObserver),
  pause offscreen, clear every timer on teardown.
- **Reduced motion** (`prefers-reduced-motion: reduce`): render the final state
  immediately. No loops, no shimmer.
- **No-JS render** should show real content (final state, expanded list), not an empty box.

### Known Astro pitfalls

- **Inline object unions in frontmatter break the build.** `export type X = { … } | { … }`
  in a `.astro` file fails with `Unexpected "|"`. Declare named interfaces and
  union those (`type X = A | B`), or put the types in a `.ts` file in the component folder.

## 5. Accessibility (non-negotiable)

- Native elements first: `<button>`, `<a>`, `<details>`, `<dialog>`, `<input>`, `<table>`.
- Full keyboard support matching the WAI-ARIA Authoring Practices pattern
  (menu, listbox, combobox, tabs, switch, …).
- Visible focus via `box-shadow: var(--vs-focus-ring)` on `:focus-visible`.
- Accessible names on icon-only buttons. Use `aria-live` where upstream updates content dynamically.
- Pointer targets at least 24×24 px.

## 6. Fidelity

Match the upstream file closely: structure, spacing, type sizes, colors,
shadows, motion timings and easings. Upstream gallery demos double as specs.
Upstream "variants" become `meta.variants` plus a `variant` prop on the demo
(or on the component, when it is a real option).

## 7. Demo content

Write fresh demo copy, not the upstream ice-cream copy. Demos follow one
fictional client: **Northwind Outdoor** (`northwind.test`), a marketing team
using AI systems. Typical content: drafts in brand voice, weekly reports from
GA4, Ads and CRM data, citation tracking across ChatGPT, Perplexity, Gemini and
AI Overviews, Reddit and YouTube threads, campaign approvals. Numbers are
obvious samples. Never use real company names, client results or people.

## 8. Agent workflow

1. Work in `src/ui/registry/<slug>/` with Node 22.12+.
2. Edit only your assigned folders. Others work in parallel.
3. Verify with `npx astro check` (0 errors) and `npx astro build`.
4. Don't commit.
5. Report: what you ported, deviations from upstream and why, new licenses,
   anything unfinished.
