a template starter repo for sveltekit projects
0

Configure Feed

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

feat: install bits-ui and stylebase, scaffold UI wrappers

- pnpm add @taurean/stylebase bits-ui
- src/app.css imports stylebase and provides global focus ring
- +layout.svelte wires app.css
- src/lib/components/ui/Button.svelte and Accordion.svelte are
Svelte 5 + bits-ui wrappers (script-only agent territory)
- Storybook stories for both wrappers
- vitest-browser-svelte smoke tests for both wrappers
- .gitignore: worker-configuration.d.ts (wrangler build artifact)

Wrapper styling is intentionally minimal — human territory per
AGENTS.md. The wrappers render with stylebase defaults; the
hybrid strategy (global data-attribute CSS + per-component
overrides) is the next step.

Refs agent-notes/2026-06-03-02-install-bits-ui-stylebase.md

+360 -27
+4
.gitignore
··· 22 22 vite.config.js.timestamp-* 23 23 vite.config.ts.timestamp-* 24 24 25 + # Cloudflare / Wrangler (generated) 26 + worker-configuration.d.ts 27 + .wrangler 28 + 25 29 *storybook.log 26 30 storybook-static
+62
agent-notes/2026-06-03-02-install-bits-ui-stylebase.md
··· 1 + # install-bits-ui-stylebase 2 + 3 + ## Task 4 + 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. 5 + 6 + ## Decisions 7 + - **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. 8 + - **Scaffolded Button + Accordion wrappers** (Q2) — covers both the simple primitive and state-with-data-attributes patterns. 9 + - **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. 10 + - **No smoke test on `+page.svelte`** — Storybook is the visual smoke; touching the page markup crosses the human-boundary (AGENTS.md authoring rules). 11 + - **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.* 12 + - **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. 13 + - **`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. 14 + - **Did not run prettier on pre-existing files** — `pnpm 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. 15 + - **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). 16 + - **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. 17 + 18 + ## Actions 19 + - 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. 20 + - `pnpm add @taurean/stylebase bits-ui` — installed stylebase 0.11.0, bits-ui 2.18.1; lockfile updated. 21 + - Created `src/app.css` with `@import '@taurean/stylebase';` and a `:focus-visible` block using `--hue-blue-500`. 22 + - Edited `src/routes/+layout.svelte` to `import '../app.css';` in the `<script lang="ts">` block. 23 + - Created `src/lib/components/ui/Button.svelte` — bits-ui `Button.Root` wrapper, Svelte 5 runes, `ComponentProps` typing, `Omit` + Snippet-required pattern for children. 24 + - 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. 25 + - 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). 26 + - Replaced `src/stories/Button.stories.svelte` (was a Storybook demo) with a Suede Button story (Primary, Disabled). 27 + - Created `src/stories/Accordion.stories.svelte` with Single and Multiple stories. 28 + - 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. 29 + - Ran `pnpm gen` to seed `worker-configuration.d.ts` for the Cloudflare build to succeed. 30 + - Added `worker-configuration.d.ts` to `.gitignore` (and tightened the `.wrangler` entry). 31 + - Iterated twice on `pnpm check` errors (interface-extends union, destructure widening) and once on test snippet creation. 32 + 33 + ## Files touched 34 + - `package.json` — added `@taurean/stylebase`, `bits-ui` to dependencies 35 + - `pnpm-lock.yaml` — regenerated 36 + - `src/app.css` — created 37 + - `src/routes/+layout.svelte` — `import '../app.css';` added 38 + - `src/lib/components/ui/Button.svelte` — created 39 + - `src/lib/components/ui/Accordion.svelte` — created 40 + - `src/lib/components/ui/index.ts` — created 41 + - `src/lib/components/ui/Button.svelte.spec.ts` — created 42 + - `src/lib/components/ui/Accordion.svelte.spec.ts` — created 43 + - `src/stories/Button.stories.svelte` — replaced (Storybook demo → Suede wrapper story) 44 + - `src/stories/Accordion.stories.svelte` — created 45 + - `.gitignore` — added `worker-configuration.d.ts`; tightened `.wrangler` entry 46 + 47 + ## Verification 48 + - `pnpm check` — pass. `svelte-check found 0 errors and 0 warnings` across the workspace. 49 + - `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. 50 + - `pnpm build` — pass. Cloudflare adapter completes; client bundle is small; `_layout` CSS chunk is 16.80 kB (stylebase's contribution, gzipped 4.02 kB). 51 + - `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. 52 + - Manual type exploration: confirmed `Button.RootProps` and `Accordion.RootProps` are discriminated unions; the wrapper type patterns are documented in the Decisions section. 53 + 54 + ## Follow-ups / stubs 55 + - **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. 56 + - **Storybook demo content cleanup** — `src/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. 57 + - **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. 58 + - **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. 59 + - **More wrappers** — Dialog, Select, Tabs, Combobox are the next-highest-value bits-ui primitives for typical app work. Same scaffold pattern. 60 + - **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. 61 + - **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. 62 + - **CreateRawSnippet for tests** — works but is awkward. If wrapper test count grows, consider a `testHelpers.ts` with `textSnippet(s)` / `elementSnippet(html)` helpers.
+5 -1
package.json
··· 57 57 "vitest-browser-svelte": "^2.1.0", 58 58 "wrangler": "^4.81.0" 59 59 }, 60 - "packageManager": "pnpm@10.6.0+sha512.df0136e797db0cfa7ec1084e77f3bdf81bacbae9066832fbf95cba4c2140ad05e64f316cde51ce3f99ea00a91ffc702d6aedd3c0f450f895e3e7c052fe573cd8" 60 + "packageManager": "pnpm@10.6.0+sha512.df0136e797db0cfa7ec1084e77f3bdf81bacbae9066832fbf95cba4c2140ad05e64f316cde51ce3f99ea00a91ffc702d6aedd3c0f450f895e3e7c052fe573cd8", 61 + "dependencies": { 62 + "@taurean/stylebase": "^0.11.0", 63 + "bits-ui": "^2.18.1" 64 + } 61 65 }
+116
pnpm-lock.yaml
··· 7 7 importers: 8 8 9 9 .: 10 + dependencies: 11 + '@taurean/stylebase': 12 + specifier: ^0.11.0 13 + version: 0.11.0 14 + bits-ui: 15 + specifier: ^2.18.1 16 + version: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)) 10 17 devDependencies: 11 18 '@chromatic-com/storybook': 12 19 specifier: ^5.1.2 ··· 869 876 resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} 870 877 engines: {node: ^20.19.0 || ^22.13.0 || >=24} 871 878 879 + '@floating-ui/core@1.7.5': 880 + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} 881 + 882 + '@floating-ui/dom@1.7.6': 883 + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} 884 + 885 + '@floating-ui/utils@0.2.11': 886 + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} 887 + 872 888 '@humanfs/core@0.19.2': 873 889 resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} 874 890 engines: {node: '>=18.18.0'} ··· 1026 1042 cpu: [x64] 1027 1043 os: [win32] 1028 1044 1045 + '@internationalized/date@3.12.2': 1046 + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} 1047 + 1029 1048 '@jridgewell/gen-mapping@0.3.13': 1030 1049 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 1031 1050 ··· 1312 1331 svelte: ^5.46.4 1313 1332 vite: ^8.0.0-beta.7 || ^8.0.0 1314 1333 1334 + '@swc/helpers@0.5.23': 1335 + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} 1336 + 1337 + '@taurean/stylebase@0.11.0': 1338 + resolution: {integrity: sha512-8oJ1dmzs8gbydINRB8+tSv85Tm7OZ2GYp9HSNoRGD9tU8PgqKRzdPcIw72c4koGSKhNpgFw5OaggwiGJ+XM3Dg==} 1339 + 1315 1340 '@testing-library/dom@10.4.1': 1316 1341 resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} 1317 1342 engines: {node: '>=18'} ··· 1550 1575 resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} 1551 1576 engines: {node: 18 || 20 || >=22} 1552 1577 1578 + bits-ui@2.18.1: 1579 + resolution: {integrity: sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==} 1580 + engines: {node: '>=20'} 1581 + peerDependencies: 1582 + '@internationalized/date': ^3.8.1 1583 + svelte: ^5.33.0 1584 + 1553 1585 blake3-wasm@2.1.5: 1554 1586 resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 1555 1587 ··· 2004 2036 resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} 2005 2037 engines: {node: '>=8'} 2006 2038 2039 + inline-style-parser@0.2.7: 2040 + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} 2041 + 2007 2042 is-docker@3.0.0: 2008 2043 resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} 2009 2044 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 2358 2393 run-applescript@7.1.0: 2359 2394 resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} 2360 2395 engines: {node: '>=18'} 2396 + 2397 + runed@0.35.1: 2398 + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} 2399 + peerDependencies: 2400 + '@sveltejs/kit': ^2.21.0 2401 + svelte: ^5.7.0 2402 + peerDependenciesMeta: 2403 + '@sveltejs/kit': 2404 + optional: true 2361 2405 2362 2406 sade@1.8.1: 2363 2407 resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} ··· 2433 2477 resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} 2434 2478 engines: {node: '>=8'} 2435 2479 2480 + style-to-object@1.0.14: 2481 + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} 2482 + 2436 2483 supports-color@10.2.2: 2437 2484 resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} 2438 2485 engines: {node: '>=18'} ··· 2464 2511 svelte: 2465 2512 optional: true 2466 2513 2514 + svelte-toolbelt@0.10.6: 2515 + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} 2516 + engines: {node: '>=18', pnpm: '>=8.7.0'} 2517 + peerDependencies: 2518 + svelte: ^5.30.2 2519 + 2467 2520 svelte2tsx@0.7.55: 2468 2521 resolution: {integrity: sha512-JWzgeM3lqySRNfqcsesvVEh8LhTWBxQJ9RMjzJ+VepdmXtVnNd0SbtGctG6+/fbHq0N6mhwSd823gszw9JHeGQ==} 2469 2522 peerDependencies: ··· 2473 2526 svelte@5.55.5: 2474 2527 resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} 2475 2528 engines: {node: '>=18'} 2529 + 2530 + tabbable@6.4.0: 2531 + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} 2476 2532 2477 2533 tiny-invariant@1.3.3: 2478 2534 resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} ··· 3196 3252 '@eslint/core': 1.2.1 3197 3253 levn: 0.4.1 3198 3254 3255 + '@floating-ui/core@1.7.5': 3256 + dependencies: 3257 + '@floating-ui/utils': 0.2.11 3258 + 3259 + '@floating-ui/dom@1.7.6': 3260 + dependencies: 3261 + '@floating-ui/core': 1.7.5 3262 + '@floating-ui/utils': 0.2.11 3263 + 3264 + '@floating-ui/utils@0.2.11': {} 3265 + 3199 3266 '@humanfs/core@0.19.2': 3200 3267 dependencies: 3201 3268 '@humanfs/types': 0.15.0 ··· 3307 3374 3308 3375 '@img/sharp-win32-x64@0.34.5': 3309 3376 optional: true 3377 + 3378 + '@internationalized/date@3.12.2': 3379 + dependencies: 3380 + '@swc/helpers': 0.5.23 3310 3381 3311 3382 '@jridgewell/gen-mapping@0.3.13': 3312 3383 dependencies: ··· 3587 3658 vite: 8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0) 3588 3659 vitefu: 1.1.3(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)) 3589 3660 3661 + '@swc/helpers@0.5.23': 3662 + dependencies: 3663 + tslib: 2.8.1 3664 + 3665 + '@taurean/stylebase@0.11.0': {} 3666 + 3590 3667 '@testing-library/dom@10.4.1': 3591 3668 dependencies: 3592 3669 '@babel/code-frame': 7.29.0 ··· 3896 3973 3897 3974 balanced-match@4.0.4: {} 3898 3975 3976 + bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)): 3977 + dependencies: 3978 + '@floating-ui/core': 1.7.5 3979 + '@floating-ui/dom': 1.7.6 3980 + '@internationalized/date': 3.12.2 3981 + esm-env: 1.2.2 3982 + runed: 0.35.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)) 3983 + svelte: 5.55.5(@typescript-eslint/types@8.59.2) 3984 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)) 3985 + tabbable: 6.4.0 3986 + transitivePeerDependencies: 3987 + - '@sveltejs/kit' 3988 + 3899 3989 blake3-wasm@2.1.5: {} 3900 3990 3901 3991 brace-expansion@5.0.5: ··· 4304 4394 4305 4395 indent-string@4.0.0: {} 4306 4396 4397 + inline-style-parser@0.2.7: {} 4398 + 4307 4399 is-docker@3.0.0: {} 4308 4400 4309 4401 is-extglob@2.1.1: {} ··· 4615 4707 4616 4708 run-applescript@7.1.0: {} 4617 4709 4710 + runed@0.35.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)): 4711 + dependencies: 4712 + dequal: 2.0.3 4713 + esm-env: 1.2.2 4714 + lz-string: 1.5.0 4715 + svelte: 5.55.5(@typescript-eslint/types@8.59.2) 4716 + optionalDependencies: 4717 + '@sveltejs/kit': 2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)) 4718 + 4618 4719 sade@1.8.1: 4619 4720 dependencies: 4620 4721 mri: 1.2.0 ··· 4717 4818 dependencies: 4718 4819 min-indent: 1.0.1 4719 4820 4821 + style-to-object@1.0.14: 4822 + dependencies: 4823 + inline-style-parser: 0.2.7 4824 + 4720 4825 supports-color@10.2.2: {} 4721 4826 4722 4827 supports-color@7.2.0: ··· 4753 4858 optionalDependencies: 4754 4859 svelte: 5.55.5(@typescript-eslint/types@8.59.2) 4755 4860 4861 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)): 4862 + dependencies: 4863 + clsx: 2.1.1 4864 + runed: 0.35.1(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5(@typescript-eslint/types@8.59.2))(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@8.0.11(@types/node@22.19.17)(esbuild@0.27.7)(tsx@4.21.0)))(svelte@5.55.5(@typescript-eslint/types@8.59.2)) 4865 + style-to-object: 1.0.14 4866 + svelte: 5.55.5(@typescript-eslint/types@8.59.2) 4867 + transitivePeerDependencies: 4868 + - '@sveltejs/kit' 4869 + 4756 4870 svelte2tsx@0.7.55(svelte@5.55.5(@typescript-eslint/types@8.59.2))(typescript@5.9.3): 4757 4871 dependencies: 4758 4872 dedent-js: 1.0.1 ··· 4780 4894 zimmerframe: 1.1.4 4781 4895 transitivePeerDependencies: 4782 4896 - '@typescript-eslint/types' 4897 + 4898 + tabbable@6.4.0: {} 4783 4899 4784 4900 tiny-invariant@1.3.3: {} 4785 4901
+7
src/app.css
··· 1 + @import '@taurean/stylebase'; 2 + 3 + :focus-visible { 4 + outline: 2px solid var(--hue-blue-500); 5 + outline-offset: 2px; 6 + border-radius: 2px; 7 + }
+37
src/lib/components/ui/Accordion.svelte
··· 1 + <script lang="ts"> 2 + import { Accordion as AccordionPrimitive } from 'bits-ui'; 3 + import type { Snippet } from 'svelte'; 4 + 5 + interface Item { 6 + value: string; 7 + title: string; 8 + content: Snippet; 9 + } 10 + 11 + interface Props { 12 + items: Item[]; 13 + type?: 'single' | 'multiple'; 14 + value?: string | string[]; 15 + onValueChange?: (value: string | string[]) => void; 16 + } 17 + 18 + let { items, type = 'single', value, onValueChange, ...rest }: Props = $props(); 19 + </script> 20 + 21 + <AccordionPrimitive.Root 22 + type={type as 'single' | 'multiple'} 23 + value={value as never} 24 + onValueChange={onValueChange as never} 25 + {...rest} 26 + > 27 + {#each items as item (item.value)} 28 + <AccordionPrimitive.Item value={item.value}> 29 + <AccordionPrimitive.Header> 30 + <AccordionPrimitive.Trigger>{item.title}</AccordionPrimitive.Trigger> 31 + </AccordionPrimitive.Header> 32 + <AccordionPrimitive.Content> 33 + {@render item.content()} 34 + </AccordionPrimitive.Content> 35 + </AccordionPrimitive.Item> 36 + {/each} 37 + </AccordionPrimitive.Root>
+32
src/lib/components/ui/Accordion.svelte.spec.ts
··· 1 + import { createRawSnippet } from 'svelte'; 2 + import { page } from 'vitest/browser'; 3 + import { describe, expect, it } from 'vitest'; 4 + import { render } from 'vitest-browser-svelte'; 5 + import type { Snippet } from 'svelte'; 6 + import Accordion from './Accordion.svelte'; 7 + 8 + const text = (value: string): Snippet => 9 + createRawSnippet(() => ({ 10 + render: () => `<p>${value}</p>` 11 + })); 12 + 13 + describe('Accordion.svelte', () => { 14 + const items = [ 15 + { value: 'item-1', title: 'Section 1', content: text('First content') }, 16 + { value: 'item-2', title: 'Section 2', content: text('Second content') } 17 + ]; 18 + 19 + it('renders each item as a trigger', async () => { 20 + render(Accordion, { items, type: 'single' }); 21 + 22 + await expect.element(page.getByRole('button', { name: 'Section 1' })).toBeInTheDocument(); 23 + await expect.element(page.getByRole('button', { name: 'Section 2' })).toBeInTheDocument(); 24 + }); 25 + 26 + it('reveals content when a trigger is clicked', async () => { 27 + render(Accordion, { items, type: 'single' }); 28 + 29 + await page.getByRole('button', { name: 'Section 1' }).click(); 30 + await expect.element(page.getByText('First content')).toBeVisible(); 31 + }); 32 + });
+14
src/lib/components/ui/Button.svelte
··· 1 + <script lang="ts"> 2 + import { Button as ButtonPrimitive } from 'bits-ui'; 3 + import type { ComponentProps, Snippet } from 'svelte'; 4 + 5 + type Props = Omit<ComponentProps<typeof ButtonPrimitive.Root>, 'children'> & { 6 + children: Snippet; 7 + }; 8 + 9 + let { children, ...rest }: Props = $props(); 10 + </script> 11 + 12 + <ButtonPrimitive.Root {...rest}> 13 + {@render children()} 14 + </ButtonPrimitive.Root>
+25
src/lib/components/ui/Button.svelte.spec.ts
··· 1 + import { createRawSnippet } from 'svelte'; 2 + import { page } from 'vitest/browser'; 3 + import { describe, expect, it } from 'vitest'; 4 + import { render } from 'vitest-browser-svelte'; 5 + import Button from './Button.svelte'; 6 + 7 + const label = (text: string) => 8 + createRawSnippet(() => ({ 9 + render: () => `<span>${text}</span>` 10 + })); 11 + 12 + describe('Button.svelte', () => { 13 + it('renders its children', async () => { 14 + render(Button, { children: label('Click me') }); 15 + 16 + await expect.element(page.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); 17 + }); 18 + 19 + it('applies disabled state', async () => { 20 + render(Button, { children: label('Disabled'), disabled: true }); 21 + 22 + const button = page.getByRole('button', { name: 'Disabled' }); 23 + await expect.element(button).toBeDisabled(); 24 + }); 25 + });
+2
src/lib/components/ui/index.ts
··· 1 + export { default as Button } from './Button.svelte'; 2 + export { default as Accordion } from './Accordion.svelte';
+1
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 + import '../app.css'; 2 3 import favicon from '$lib/assets/favicon.svg'; 3 4 4 5 let { children } = $props();
+38
src/stories/Accordion.stories.svelte
··· 1 + <script module> 2 + import { defineMeta } from '@storybook/addon-svelte-csf'; 3 + import Accordion from '$lib/components/ui/Accordion.svelte'; 4 + 5 + const { Story } = defineMeta({ 6 + title: 'UI/Accordion', 7 + component: Accordion, 8 + tags: ['autodocs'] 9 + }); 10 + </script> 11 + 12 + {#snippet sectionOne()} 13 + <p>Content for section 1.</p> 14 + {/snippet} 15 + 16 + {#snippet sectionTwo()} 17 + <p>Content for section 2.</p> 18 + {/snippet} 19 + 20 + <Story name="Single"> 21 + <Accordion 22 + items={[ 23 + { value: 'item-1', title: 'Section 1', content: sectionOne }, 24 + { value: 'item-2', title: 'Section 2', content: sectionTwo } 25 + ]} 26 + type="single" 27 + /> 28 + </Story> 29 + 30 + <Story name="Multiple"> 31 + <Accordion 32 + items={[ 33 + { value: 'item-1', title: 'Section 1', content: sectionOne }, 34 + { value: 'item-2', title: 'Section 2', content: sectionTwo } 35 + ]} 36 + type="multiple" 37 + /> 38 + </Story>
+17 -26
src/stories/Button.stories.svelte
··· 1 1 <script module> 2 - import { defineMeta } from '@storybook/addon-svelte-csf'; 3 - import Button from './Button.svelte'; 4 - import { fn } from 'storybook/test'; 2 + import { defineMeta } from '@storybook/addon-svelte-csf'; 3 + import Button from '$lib/components/ui/Button.svelte'; 4 + import { fn } from 'storybook/test'; 5 5 6 - // More on how to set up stories at: https://storybook.js.org/docs/writing-stories 7 - const { Story } = defineMeta({ 8 - title: 'Example/Button', 9 - component: Button, 10 - tags: ['autodocs'], 11 - argTypes: { 12 - backgroundColor: { control: 'color' }, 13 - size: { 14 - control: { type: 'select' }, 15 - options: ['small', 'medium', 'large'], 16 - }, 17 - }, 18 - args: { 19 - onclick: fn(), 20 - } 21 - }); 6 + const { Story } = defineMeta({ 7 + title: 'UI/Button', 8 + component: Button, 9 + tags: ['autodocs'], 10 + args: { 11 + onclick: fn() 12 + } 13 + }); 22 14 </script> 23 15 24 - <!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args --> 25 - <Story name="Primary" args={{ primary: true, label: 'Button' }} /> 16 + <Story name="Primary"> 17 + <Button>Click me</Button> 18 + </Story> 26 19 27 - <Story name="Secondary" args={{ label: 'Button' }} /> 28 - 29 - <Story name="Large" args={{ size: 'large', label: 'Button' }} /> 30 - 31 - <Story name="Small" args={{ size: 'small', label: 'Button' }} /> 20 + <Story name="Disabled"> 21 + <Button disabled>Disabled</Button> 22 + </Story>