# 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.

## 9. Sections

A section is a full-width page block (`kind: "section"`): hero, feature grid,
pricing, FAQ and so on. Everything above applies, plus:

- **Respond to the container, not the viewport.** The root gets
  `container-type: inline-size` and layout changes use `@container`
  queries, so a section behaves the same in a gallery card, a column or a
  full page. Size type and spacing with `cqi` and `clamp()`.
- **One layout per block.** Each distinct design is its own registry entry
  (`hero-statement`, `hero-split`, …) so it can be copied as one file and
  browsed by category. Use props for small options (`reverse`, `tone`), not
  for switching between layouts.
- **Look comes from tokens.** Headlines use the `.vs-display` class from
  `foundation.css`; borders use `--vs-border-w`; hard or glowing shadows
  come through `--vs-pop-shadow`. Never branch on a theme name. Presets in
  `src/ui/styles/themes.css` (`data-vs-theme="editorial|brutalist|soft|midnight"`)
  restyle every section, so check a new section in all of them.
- **Headline = `title` + optional `accent`.** The accent words render in
  `<em>`, which each theme draws differently (italic serif, highlighter).
- **Content in, not baked in.** Copy, links and lists are props; media is a
  named slot. No images or fonts are bundled in a section. Demos use the
  Northwind art in `public/ui/demo/` (from `scripts/visuals/ui_demo_art.py`),
  never the Viewership sculptures.
- **Semantic and no-JS.** Native `<details>`, lists, figures and headings
  (`as` prop for the heading level). Sections should need no script.
- **Structured data where it earns its place** (FAQ emits FAQPage JSON-LD,
  with `schema={false}` to opt out).
- Categories: Hero, Features, Proof, Pricing, Conversion, Site chrome. Aim for
  three or more blocks in each.

