Suede#
SvelteKit starter template. Entry point for stack, conventions, and process: AGENTS.md.
For self-hosted knots, clone URLs may differ based on your setup.
Two-part fix for the same observable bug: SuedeButton rendered
with browser-default styles in Storybook (gray bg, black text,
0 padding, Arial font) because the stylebase custom properties
(--hue-blue-500, --space-*, --ff-ui, etc.) and utility classes
(u:fs-1) were never present in the Storybook preview iframe.
Part 1 — .storybook/preview.ts:
Import `../src/app.css` so the Storybook preview iframe gets
the same global stylesheet as the SvelteKit app (stylebase +
the :focus-visible rule). Storybook's preview iframe is
independent of the SvelteKit app's +layout.svelte, so
SvelteKit's import chain didn't reach it.
Part 2 — src/lib/components/ui/SuedeButton.svelte:
Wrap every .suede-button selector in :global(...). The class
is set as a prop on the child <Button.Root> component, not
as a class on SuedeButton's own template element. Svelte 5's
per-component CSS scoping only applies to elements rendered
by that component — the .suede-button rule was being pruned
(0 selectors in the bundle) even with the svelte-ignore
comment, because svelte-ignore suppresses the warning but
doesn't prevent the build-time strip.
:global() escapes the scoping. The `suede-button` class name
is unique enough that global leakage is safe.
Verified via Playwright on all 4 SuedeButton stories + style
introspection (computed styles + styleSheets inspection):
Primary bg=oklch(.7188 .15 240) [blue] cursor=pointer
Disabled same blue, cursor=not-allowed
As link renders <a>, cursor=pointer
With class override bg=oklch(.7109 .15 140) [emerald], font-size=20.1px
(u:fs-3 override of u:fs-1), class= 'suede-button u:fs-1 u:fs-3'
All custom properties now resolve at :root, all .suede-button
rules present in the bundle, pnpm check still 0/0 project-wide.
`src/stories/Button.stories.svelte` and `src/stories/Accordion.stories.svelte`
both imported `$lib/components/ui/{Button,Accordion}.svelte` which no longer
exist on this branch (deleted in working tree). The deletion left the
stories with unresolvable imports, and Storybook's vite import-analysis
fails at build time on those files (the same asChild-related issue
surfaced earlier would have hit if the broken imports were repaired).
`src/stories/Page.stories.svelte` and `src/stories/Header.stories.svelte`
are kept: their imports are local (`./Page.svelte`, `./Header.svelte`)
and still resolve.
Verified via Playwright across 6 story URLs (all 4 SuedeButton + the two
remaining examples): 0 console errors, 0 vite import-analysis errors.
Only warnings are Storybook 11 deprecations on Storybook's internal UI
(`ariaLabel` on its own Button/PopoverProvider), not user code.
`pnpm check` is now fully clean (0 errors, 0 warnings) — no more
pre-existing noise from the deleted stories.
@storybook/addon-svelte-csf's Story.svelte, when given both a
`component` in defineMeta and children in the <Story> block, wraps
the children in another instance of that component
(line 148: `<renderer.storyContext.component {...renderer.args} {children} />`).
In our case `component: SuedeButton` + `<SuedeButton>Click me</SuedeButton>`
as Story children produced `<button class="suede-button"><button class="suede-button">Click me</button></button>`
— an accessibility violation (interactive element nested in interactive element)
and semantically wrong markup.
`asChild` on each <Story> routes the children through the
`{@render children()}` branch (no component wrapping) so the story
renders as static markup. Trade-off: args-driven controls don't
update the canvas for `asChild` stories, which is acceptable for
discrete-variant learning demos.
Verified via Playwright against the running Storybook dev server:
Primary / Disabled / With-class-override each render exactly one
`<button data-button-root="true" class="suede-button ...">` at the
root of the story canvas; As link renders as a single
`<a data-button-root="true">`. `button button` and `a button`
nested-selector counts: 0 in every story.
Adds a Suede-wrapped bits-ui Button as a learning scaffold for the
bits-ui + stylebase + Svelte 5 pattern. Distinguishes functionality
(bits-ui's polymorphic Button.Root via ButtonRootProps) from style
(stylebase utility class + scoped <style> block in the same file).
Story at src/stories/SuedeButton.stories.svelte exercises four
variants: Primary, Disabled, As link, With class override.
TypeScript escape hatch: rest is spread via
`{...rest as Record<string, unknown>}` because bits-ui's
ButtonRootProps is a union (AnchorElement | ButtonElement) that
exceeds TypeScript's type complexity budget when re-spread into
another Svelte component. Cast is local to one line; polymorphic
behavior (button vs anchor via href) still works at runtime.
CSS pruner workaround: `<!-- svelte-ignore css_unused_selector -->`
on the <style> block, because the class name is built from a
template literal in the class attribute and is therefore invisible
to Svelte's static class-extraction pass.
Existing example stories (Button, Accordion, Header, Page) are
left untouched per option C; pre-existing pnpm check errors
(missing /components/ui/Button.svelte and Accordion.svelte
references) are out of scope for this change.
SvelteKit starter template. Entry point for stack, conventions, and process: AGENTS.md.