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/componentsis 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 undergyrum-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>>
-->
### 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:
- Update
gyrum-labs/canon/data/manifests/gyrum-ui.yaml— add an entry toowns:withname: <kebab-name>+tier: <atom|molecule|...>+ optionaltracking_ticket: warp#NNN
For lifts (ai-frontend or other consumer retiring a local copy):
- Update the consumer's
canon-manifest.yamllifts:block withname: <kebab-name>+lift_state: <state>+tracking_ticket: warp#NNN - As consumers retire, flip
lift_statetowardretired(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.tsMUST includepage.on('pageerror', e => throw e)inline ORimport { assertNoConsoleErrors } from './helpers/console'used intest.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
headingrename 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