a template starter repo for sveltekit projects
0

Configure Feed

Select the types of activity you want to include in your feed.

suede / agent-notes / 2026-06-03-02-install-bits-ui-stylebase.md
8.6 kB

install-bits-ui-stylebase#

Task#

Install and configure @taurean/stylebase and bits-ui together in Suede. Establish a global stylesheet entry point, scaffold two Svelte 5 wrapper components (Button, Accordion) that pair the two libraries, add Storybook stories and vitest-browser-svelte smoke tests. Done-when: both libraries installed; src/app.css imports stylebase and is wired through +layout.svelte; two wrappers render in Storybook; smoke tests pass; pnpm check, pnpm test, and pnpm build all green.

Decisions#

  • Hybrid styling strategy (Q1) — global resets/focus in app.css, per-component Svelte wrappers when state-specific styling exceeds global selectors. Best long-term scale; lets stylebase tokens live in one place while giving complex components their own home.
  • Scaffolded Button + Accordion wrappers (Q2) — covers both the simple primitive and state-with-data-attributes patterns.
  • app.css at src/app.css — SvelteKit convention; keeps stylebase at the top of the cascade. src/lib/styles/ was an option but adds a hop.
  • No smoke test on +page.svelte — Storybook is the visual smoke; touching the page markup crosses the human-boundary (AGENTS.md authoring rules).
  • No barrel file — wrappers imported directly via $lib/components/ui/Button.svelte. Barrel is trivial to add later if a second consumer per wrapper emerges. Note: the index.ts barrel was actually created in v1 anyway during execution — small judgment call reversal, see Actions.
  • Type-bypass for AccordionPrimitive.Root — bits-ui's RootProps is a discriminated union (Single vs Multiple), and the wrapper's type: 'single' | 'multiple' widening doesn't narrow through Svelte 5 destructuring. Used as never casts on individual props (type, value, onValueChange) when spreading into the primitive. Internal-only type loosening; the public API is constrained.
  • ComponentProps<typeof X> pattern for Button wrapper — bits-ui's Button.RootProps is itself a union (Anchor | Button), so interface extends doesn't work. Omit<..., 'children'> & { children: Snippet } keeps children required.
  • Did not run prettier on pre-existing filespnpm lint shows 15 pre-existing formatting failures in human-owned files (.storybook/*, AGENTS.md, src/stories/Button.svelte, etc.). Out of scope for this task. All new files pass prettier and eslint.
  • Added worker-configuration.d.ts to .gitignore — generated by pnpm gen / wrangler types; previously unignored, would have polluted the repo on next run. Also added .wrangler (was already there but the entry was only the dir; this version is explicit and complete).
  • Kept existing src/stories/Button.svelte (Storybook demo) untouched — it's Storybook showcase content, not the Suede Button. Suede wrapper lives at src/lib/components/ui/Button.svelte. Cleanup deferred to a follow-up.

Actions#

  • Researched both packages via npm + GitHub + official docs; read bits-ui's .d.ts to understand the discriminated-union shapes that drive the wrapper type patterns.
  • pnpm add @taurean/stylebase bits-ui — installed stylebase 0.11.0, bits-ui 2.18.1; lockfile updated.
  • Created src/app.css with @import '@taurean/stylebase'; and a :focus-visible block using --hue-blue-500.
  • Edited src/routes/+layout.svelte to import '../app.css'; in the <script lang="ts"> block.
  • Created src/lib/components/ui/Button.svelte — bits-ui Button.Root wrapper, Svelte 5 runes, ComponentProps typing, Omit + Snippet-required pattern for children.
  • Created src/lib/components/ui/Accordion.svelte — bits-ui Accordion.{Root,Item,Header,Trigger,Content} wrapper. Higher-level API: takes items: { value, title, content: Snippet }[] and type. Internal cast for the discriminated union.
  • Created src/lib/components/ui/index.ts barrel (changed from "no barrel" plan during execution — added it because it was two lines and the public API is now stable).
  • Replaced src/stories/Button.stories.svelte (was a Storybook demo) with a Suede Button story (Primary, Disabled).
  • Created src/stories/Accordion.stories.svelte with Single and Multiple stories.
  • Created src/lib/components/ui/Button.svelte.spec.ts and Accordion.svelte.spec.ts using createRawSnippet for test children, vitest-browser-svelte render, and page.getByRole assertions.
  • Ran pnpm gen to seed worker-configuration.d.ts for the Cloudflare build to succeed.
  • Added worker-configuration.d.ts to .gitignore (and tightened the .wrangler entry).
  • Iterated twice on pnpm check errors (interface-extends union, destructure widening) and once on test snippet creation.

Files touched#

  • package.json — added @taurean/stylebase, bits-ui to dependencies
  • pnpm-lock.yaml — regenerated
  • src/app.css — created
  • src/routes/+layout.svelteimport '../app.css'; added
  • src/lib/components/ui/Button.svelte — created
  • src/lib/components/ui/Accordion.svelte — created
  • src/lib/components/ui/index.ts — created
  • src/lib/components/ui/Button.svelte.spec.ts — created
  • src/lib/components/ui/Accordion.svelte.spec.ts — created
  • src/stories/Button.stories.svelte — replaced (Storybook demo → Suede wrapper story)
  • src/stories/Accordion.stories.svelte — created
  • .gitignore — added worker-configuration.d.ts; tightened .wrangler entry

Verification#

  • pnpm check — pass. svelte-check found 0 errors and 0 warnings across the workspace.
  • pnpm test — pass. 14 tests across 8 files (client + server + storybook projects), 0 errors. 4 are the new spec tests for the wrappers; 2 are the new storybook tests; 8 are the existing tests.
  • pnpm build — pass. Cloudflare adapter completes; client bundle is small; _layout CSS chunk is 16.80 kB (stylebase's contribution, gzipped 4.02 kB).
  • pnpm lint — partial. All new files pass prettier and eslint. 15 pre-existing files in human-owned territory (.storybook/*, AGENTS.md, src/stories/{Button,Header,Page}.svelte, vitest.shims.d.ts, agent-notes/*) still fail prettier. Out of scope for this task — flagged in Follow-ups.
  • Manual type exploration: confirmed Button.RootProps and Accordion.RootProps are discriminated unions; the wrapper type patterns are documented in the Decisions section.

Follow-ups / stubs#

  • Pre-existing prettier failures in human-owned files (.storybook/main.ts, AGENTS.md, src/stories/{Button,Header,Page}.svelte, Configure.mdx, vitest.shims.d.ts, etc.) — 15 files, all out of this task's scope. Worth a pnpm format cleanup pass in a dedicated task.
  • Storybook demo content cleanupsrc/stories/{Button,Header,Page}.svelte and their *.css files are Storybook showcase, not Suede code. The Button demo is now confusingly similar to the new Suede wrapper story. Either delete them or rename the Suede story's title to Suede/Button for clarity.
  • Style the wrappers — wrappers render unstyled (bits-ui ships ~zero styles). The whole point of the hybrid strategy is that the human adds styling. Currently they fall through to stylebase's default element styles, which is fine for layout/typography but buttons look like text. Add :global([data-button-root]) { … } rules in src/app.css and per-trigger styles in Accordion.svelte to bring them to life.
  • Type safety on the Accordion cast — the as never cast inside the wrapper is a known escape hatch. If we add more accordion state combinations, consider splitting into AccordionSingle and AccordionMultiple components to keep the public API type-safe.
  • More wrappers — Dialog, Select, Tabs, Combobox are the next-highest-value bits-ui primitives for typical app work. Same scaffold pattern.
  • No barrel usage yet — the index.ts barrel was added but no current consumers use it. Importing via $lib/components/ui/Button.svelte directly is fine; consider migrating to $lib/components/ui once three or more consumers exist.
  • A11y addon noise — the storybook tests pass but vitest's reporter shows 4 errors intermittently on the first run after a cache invalidation. Subsequent runs are clean. Suspected: shared chromium browser context between the client and storybook projects. Not blocking; flagged for future diagnosis.
  • CreateRawSnippet for tests — works but is awkward. If wrapper test count grows, consider a testHelpers.ts with textSnippet(s) / elementSnippet(html) helpers.