Guidelines

Svelte Component Guideline

**Version: v2** **Applies to: any new canonical primitive landing in `gyrum-labs/gyrum-ui` (Svelte 5 source).**

v2

Svelte Component Guideline

Version: v2 Applies to: any new canonical primitive landing in gyrum-labs/gyrum-ui (Svelte 5 source).

This guideline encodes the conventions proven by canon-tier substrate work (warp#2284 EPIC, 2026-05-12) and updated by ADR-182 (Svelte-native canon-tier; supersedes ADR-181) on 2026-05-14. PRs touching gyrum-ui/packages/svelte/** should cite Guideline: svelte-component@v2 in the PR body so persona reviewers (ADR-115) can verify against this exact version.

ADR-182 note. The legacy Lit primitive package @gyrum-labs/components is in maintenance — existing components stay published for the canon-static chrome that imports them today (gy-hero, gy-list-row, gy-badge, gy-empty-state, etc., resolved via app.html's importmap), but no new primitive lifts there. Every new canon-tier primitive is authored as Svelte source under gyrum-ui/packages/svelte.

Tier composition (atomic design — warp#2278 / ADR-006)

  • atom imports nothing
  • molecule imports atoms
  • organism imports molecules + atoms
  • template imports organisms + molecules + atoms
  • page imports exactly one template

Hard rule: a tier may not import from a higher tier (no atom importing a molecule).

Authoring contract — gyrum-ui Svelte primitive

File path: packages/svelte/src/Gy<PascalName>.svelte

Required shape:

<!--
  @component Gy<PascalName> — one-line summary.

  Longer description: when to use, when not to use, design rationale.

  Example:

  ```svelte
  <Gy<PascalName> <camelAttr>="x" testId="y">
    ...slot content...
  </Gy<PascalName>>

-->

{#if children}{@render children()}{/if}

### Slot-first composition

Prefer Svelte snippets (`children`, named snippets) for content; reserve typed props for size / variant / count / tone. Named snippets only when the primitive has multiple distinct content regions (e.g. `header` + `body`).

### Native-attribute collisions

If your contract uses a name that's also a native HTML attribute (`title`, `value`, `disabled`, `name`), RENAME to avoid collision when the prop spreads onto a host element. Precedent: gy-page-empty-state uses `heading` not `title` (native `title` triggers browser tooltip on every host element). Document the rename inline.

### Tokens, not raw values

Consume design tokens (`var(--text)`, `var(--border)`, `var(--space-4)`, `var(--radius)`) instead of raw colors/dimensions. The canon `/tokens` page is the canonical inventory.

## Peer files (required)

- `Gy<PascalName>.test.ts` — Svelte 5 unit tests
  - Mounts via Svelte 5 `mount()`
  - Renders with required props
  - Asserts slot rendering
  - Asserts `testId` forwarded as `data-testid`

- `Gy<PascalName>.stories.ts` — 3 stories minimum: default + edge cases

- `gyrum-ui/docs/components/<kebab-name>.md` — doc page (linked from canon's component detail page via the registry)

### Export shape

Add the component to:
- `packages/svelte/src/index.ts` — alphabetic `export { default as Gy<PascalName> } from './Gy<PascalName>.svelte';`
- `packages/svelte/package.json` exports map — subpath only:
  ```json
  "./Gy<PascalName>.svelte": "./src/Gy<PascalName>.svelte"

NO . barrel. The consumer's import path is @gyrum-labs/svelte/Gy<PascalName>.svelte. The barrel form import { Gy<PascalName> } from '@gyrum-labs/svelte' does NOT resolve and any PR using it will fail at npm install / vite build time. This was discovered during the page-hero retirement chain.

Naming conventions

  • Source file: PascalCase (GyPageEmptyState.svelte)
  • Export name: PascalCase identical to filename stem (GyPageEmptyState)
  • Kebab name used for data-testid, CSS class hooks, and docs: kebab-case (gy-page-empty-state)

Canon registry workflow

In the SAME PR that ships the primitive:

  1. Update gyrum-labs/canon/data/manifests/gyrum-ui.yaml — add an entry to owns: with name: <kebab-name> + tier: <atom|molecule|...> + optional tracking_ticket: warp#NNN

For lifts (ai-frontend or other consumer retiring a local copy):

  1. Update the consumer's canon-manifest.yaml lifts: block with name: <kebab-name> + lift_state: <state> + tracking_ticket: warp#NNN
  2. As consumers retire, flip lift_state toward retired (or remove the lift entry if there's no local copy left)

Testing requirements

  • Svelte unit test: required at Gy<PascalName>.test.ts
  • Smoke test (warp#974): if the primitive is consumed in any ai-frontend route, the route's tests/e2e/<route>.spec.ts MUST include page.on('pageerror', e => throw e) inline OR import { assertNoConsoleErrors } from './helpers/console' used in test.beforeEach. The smoke gate refuses presence-only specs.

PR body citation

Required line in the PR body:

Guideline: svelte-component@v2

The persona reviewer fetches this guideline version and verifies the implementation against this exact text. Bumping the guideline version is a separate PR that includes a regression-corpus re-run.

Revision process

This document is versioned. Updates ride gyrum-revise-guideline svelte-component (separate devtools wrapper, separate ticket). Hand-edits without that wrapper skip the regression suite.

References

  • ADR-006 (atomic design tiers)
  • ADR-115 (principle-aware reviewers)
  • ADR-117 (module guidelines)
  • ADR-181 (canon-tier Lit-primitive contract — superseded by ADR-182 after 30 minutes; preserved as the captured fast-reversal precedent)
  • ADR-182 (Svelte-native canon-tier; supersedes ADR-181)
  • warp#2278 (canon-tier rule)
  • warp#2376 (canon-tier-gate advisory → enforce promotion warp#2399)
  • warp#2398 (gy-page-empty-state — exemplar port)
  • warp#2394 (gy-page-hero — exemplar port + the heading rename precedent)
  • warp#2771 (this Lit-as-authoring-pattern doc cleanup pass)
  • canon site: https://canon.gyrum.ai/
  • registry endpoint: https://canon.gyrum.ai/data/migrations.json