diff --git a/docs/superpowers/plans/2026-06-20-turbo-component-library.md b/docs/superpowers/plans/2026-06-20-turbo-component-library.md new file mode 100644 index 0000000..e53586c --- /dev/null +++ b/docs/superpowers/plans/2026-06-20-turbo-component-library.md @@ -0,0 +1,436 @@ +# Turbo Component Library Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Convert the single-package Dimension Lab website into a Turborepo workspace where `apps/web` consumes a compiled reusable React component library from `packages/ui`. + +**Architecture:** The root becomes a private Bun workspace with Turbo orchestration. `packages/ui` owns generic reusable UI components, CSS, theme helpers, Storybook, and package-level tests. `apps/web` owns the website runtime, model/server/database logic, the model-to-UI adapter, e2e tests, and deployment container. + +**Tech Stack:** Bun workspaces, Turborepo, Vite, React 19, TypeScript, Storybook React Vite, Tailwind CSS v4, shadcn CSS, Vitest, Playwright, Drizzle ORM, Bun SQLite. + +--- + +## File Structure + +- Create root `turbo.json`: cacheable task graph for `build`, `check`, `test:unit`, `build-storybook`, `test:e2e`, and `test:qa`. +- Create root `tsconfig.base.json`: shared strict TypeScript defaults. +- Modify root `package.json`: private workspace root with `apps/*` and `packages/*`, Turbo scripts, and `turbo` dev dependency only. +- Create `packages/ui/package.json`: compiled `@dimensionlab/ui` package with code and CSS exports. +- Create `packages/ui/tsconfig.json` and `packages/ui/tsconfig.build.json`: package typecheck and declaration/JS build config. +- Create `packages/ui/src/styles.css`: library style entry importing font, uPlot CSS, tokens, and component CSS. +- Move `src/lib/ui/components/**` to `packages/ui/src/components/**`. +- Move `src/lib/ui/stories/**` to `packages/ui/src/stories/**`. +- Move `src/lib/ui/tokens.css` to `packages/ui/src/tokens.css`. +- Move `src/lib/ui/theme.ts`, `types.ts`, `format.ts`, `fixtures.ts`, and `index.ts` to `packages/ui/src/**`. +- Move `src/lib/ui/components/styles.css` to `packages/ui/src/components/styles.css`. +- Move `.storybook/**` to `packages/ui/.storybook/**`. +- Move unused shadcn primitives from `src/lib/components/ui/**` to `packages/ui/src/primitives/**` and update their `cn` import to package-local `src/utils.ts`. +- Move `src/lib/utils.ts` to `packages/ui/src/utils.ts`. +- Move app runtime files into `apps/web`: `src/App.tsx`, `src/main.tsx`, `src/app.css`, `src/server/**`, `src/lib/model/**`, `src/lib/server/**`, `src/lib/testing/**`, `src/vite-env.d.ts`, `src/page.test.tsx`, `src/server/dev.ts`, `tests/**`, `drizzle/**`, `Containerfile`, `index.html`, `playwright.config.ts`, `vite.config.ts`, `drizzle.config.ts`, and app-specific README/deployment files. +- Move `src/lib/ui/model-renderer.ts` and `model-renderer.test.ts` into `apps/web/src/lib/ui-adapter/**`. +- Create `apps/web/package.json`, `apps/web/tsconfig.json`, `apps/web/vite.config.ts`, and `apps/web/playwright.config.ts`. +- Update `apps/web/src/App.tsx` to import components/types from `@dimensionlab/ui` and import the adapter from `$lib/ui-adapter/model-renderer`. +- Update `apps/web/src/app.css` to import `@dimensionlab/ui/styles.css` instead of local UI CSS files. +- Update tests that read paths so package boundary tests inspect `packages/ui` and app tests inspect `apps/web`. +- Update `README.md` to describe the workspace commands, package boundaries, Storybook location, and deployment path. + +## Task 1: Workspace And Boundary Tests + +**Files:** +- Modify: `package.json` +- Create: `turbo.json` +- Create: `tsconfig.base.json` +- Create: `packages/ui/package.json` +- Create: `packages/ui/tsconfig.json` +- Create: `packages/ui/tsconfig.build.json` +- Create: `apps/web/package.json` +- Create: `apps/web/tsconfig.json` +- Create: `apps/web/src/lib/workspace-boundary.test.ts` + +- [ ] **Step 1: Write the failing workspace boundary test** + +Create `apps/web/src/lib/workspace-boundary.test.ts`: + +```ts +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; + +const root = join(import.meta.dir, "../../../.."); + +describe("workspace boundaries", () => { + test("declares the root as a turbo-managed bun workspace", () => { + const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { + private?: boolean; + scripts?: Record; + workspaces?: string[]; + }; + + expect(packageJson.private).toBe(true); + expect(packageJson.workspaces).toEqual(["apps/*", "packages/*"]); + expect(packageJson.scripts?.build).toBe("turbo build"); + expect(existsSync(join(root, "turbo.json"))).toBe(true); + }); + + test("keeps the website app and reusable UI library as separate packages", () => { + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { dependencies?: Record; name?: string }; + const uiPackage = JSON.parse( + readFileSync(join(root, "packages/ui/package.json"), "utf8"), + ) as { exports?: Record; name?: string }; + + expect(webPackage.name).toBe("@dimensionlab/web"); + expect(webPackage.dependencies?.["@dimensionlab/ui"]).toBe("workspace:*"); + expect(uiPackage.name).toBe("@dimensionlab/ui"); + expect(uiPackage.exports).toHaveProperty("."); + expect(uiPackage.exports).toHaveProperty("./styles.css"); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: + +```sh +bunx vitest run apps/web/src/lib/workspace-boundary.test.ts +``` + +Expected: FAIL because `apps/web`, `packages/ui`, and `turbo.json` do not exist. + +- [ ] **Step 3: Add minimal workspace manifests** + +Create root `package.json` as the workspace orchestrator: + +```json +{ + "name": "dimensionlab", + "version": "0.0.1", + "private": true, + "type": "module", + "packageManager": "bun@1.3.14", + "workspaces": ["apps/*", "packages/*"], + "scripts": { + "dev": "turbo dev --filter=@dimensionlab/web", + "build": "turbo build", + "preview": "bun --cwd apps/web run preview", + "storybook": "turbo storybook --filter=@dimensionlab/ui", + "build-storybook": "turbo build-storybook --filter=@dimensionlab/ui", + "check": "turbo check", + "test": "turbo test:unit", + "test:unit": "turbo test:unit", + "test:e2e": "turbo test:e2e --filter=@dimensionlab/web", + "test:qa": "turbo test:qa", + "db:generate": "bun --cwd apps/web run db:generate", + "db:check": "bun --cwd apps/web run db:check" + }, + "devDependencies": { + "turbo": "^2.5.0" + } +} +``` + +Create `turbo.json`: + +```json +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "build/**"] + }, + "check": { + "dependsOn": ["^build"], + "outputs": [] + }, + "test:unit": { + "dependsOn": ["^build"], + "outputs": [] + }, + "build-storybook": { + "dependsOn": ["^build"], + "outputs": ["storybook-static/**"] + }, + "test:e2e": { + "dependsOn": ["build", "^build"], + "outputs": ["test-results/**", "playwright-report/**"] + }, + "test:qa": { + "dependsOn": ["check", "test:unit", "build", "build-storybook", "test:e2e"], + "outputs": [] + }, + "dev": { + "cache": false, + "persistent": true + }, + "storybook": { + "cache": false, + "persistent": true + } + } +} +``` + +Create minimal `packages/ui/package.json` with `@dimensionlab/ui` exports and +minimal `apps/web/package.json` with `@dimensionlab/ui` as a workspace +dependency. Move the full dependency lists in Task 2 and Task 3. + +- [ ] **Step 4: Run the boundary test** + +Run: + +```sh +bunx vitest run apps/web/src/lib/workspace-boundary.test.ts +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```sh +git add package.json turbo.json tsconfig.base.json apps/web/package.json apps/web/tsconfig.json apps/web/src/lib/workspace-boundary.test.ts packages/ui/package.json packages/ui/tsconfig.json packages/ui/tsconfig.build.json +git commit -m "build: add turbo workspace manifests" +``` + +## Task 2: Extract The UI Package + +**Files:** +- Move: `src/lib/ui/components/**` to `packages/ui/src/components/**` +- Move: `src/lib/ui/stories/**` to `packages/ui/src/stories/**` +- Move: `src/lib/ui/{index.ts,types.ts,theme.ts,format.ts,fixtures.ts,tokens.css}` to `packages/ui/src/**` +- Move: `.storybook/**` to `packages/ui/.storybook/**` +- Move: `src/lib/components/ui/**` to `packages/ui/src/primitives/**` +- Move: `src/lib/utils.ts` to `packages/ui/src/utils.ts` +- Create: `packages/ui/src/styles.css` +- Modify: `packages/ui/src/index.ts` +- Modify: `packages/ui/src/storybook.test.ts` +- Modify: `packages/ui/src/content-boundary.test.ts` + +- [ ] **Step 1: Write the failing UI isolation assertion** + +Update the package boundary tests to read from `packages/ui/src` and assert no +imports from `apps/web`, `$lib/server`, or `$lib/model` exist: + +```ts +expect(source).not.toMatch(/from ["'](?:apps\/web|\$lib\/server|\$lib\/model)/); +``` + +Run: + +```sh +bunx vitest run packages/ui/src/content-boundary.test.ts +``` + +Expected: FAIL until the files move and the app-specific adapter is removed. + +- [ ] **Step 2: Move generic UI files** + +Run mechanical moves with `git mv`. Move `model-renderer.ts` out of the UI +package in Task 3 rather than into `packages/ui`. + +- [ ] **Step 3: Add the UI style entry** + +Create `packages/ui/src/styles.css`: + +```css +@import "@fontsource-variable/geist"; +@import "uplot/dist/uPlot.min.css"; +@import "./tokens.css"; +@import "./components/styles.css"; +``` + +- [ ] **Step 4: Make package imports relative or package-local** + +Update moved primitive files to import `cn` from `../utils`. +Remove `dashboardDocumentToUiDashboard` from `packages/ui/src/index.ts`. + +- [ ] **Step 5: Build the package** + +Run: + +```sh +bun --cwd packages/ui run build +bun --cwd packages/ui run check +bun --cwd packages/ui run test:unit +``` + +Expected: PASS and `packages/ui/dist` contains `index.js`, `index.d.ts`, and +CSS files. + +- [ ] **Step 6: Commit** + +```sh +git add packages/ui src/lib/ui src/lib/components src/lib/utils.ts +git commit -m "refactor(ui): extract reusable component package" +``` + +## Task 3: Move The Web App Workspace + +**Files:** +- Move: `src/**` app files that are not reusable UI to `apps/web/src/**` +- Move: `tests/**` to `apps/web/tests/**` +- Move: `drizzle/**` to `apps/web/drizzle/**` +- Move: `Containerfile` to `apps/web/Containerfile` +- Move: `index.html`, `vite.config.ts`, `playwright.config.ts`, `drizzle.config.ts` to `apps/web/**` +- Create: `apps/web/src/lib/ui-adapter/model-renderer.ts` +- Modify: `apps/web/src/App.tsx` +- Modify: `apps/web/src/app.css` + +- [ ] **Step 1: Move the model adapter out of UI** + +Move `src/lib/ui/model-renderer.ts` to +`apps/web/src/lib/ui-adapter/model-renderer.ts` and update imports from +`$lib/model` plus UI types from `@dimensionlab/ui`. + +- [ ] **Step 2: Move app runtime and tests** + +Use `git mv` for app/server/model/test/deployment files into `apps/web`. + +- [ ] **Step 3: Update app imports** + +In `apps/web/src/App.tsx`, import UI components and types from +`@dimensionlab/ui`, and import `dashboardDocumentToUiDashboard` from +`$lib/ui-adapter/model-renderer`. + +In `apps/web/src/app.css`, replace local UI imports with: + +```css +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@import "@dimensionlab/ui/styles.css"; +``` + +- [ ] **Step 4: Run app typecheck and unit tests** + +Run: + +```sh +bun --cwd apps/web run check +bun --cwd apps/web run test:unit +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```sh +git add apps/web src tests drizzle Containerfile index.html vite.config.ts playwright.config.ts drizzle.config.ts +git commit -m "refactor(web): move website into app workspace" +``` + +## Task 4: Wire Turbo, Storybook, Playwright, And Container + +**Files:** +- Modify: `apps/web/playwright.config.ts` +- Modify: `apps/web/Containerfile` +- Modify: `apps/web/vite.config.ts` +- Modify: `packages/ui/.storybook/main.ts` +- Modify: `packages/ui/.storybook/preview.ts` +- Modify: `tests/e2e/storybook-server.ts` after move to `apps/web/tests/e2e/storybook-server.ts` +- Modify: `README.md` + +- [ ] **Step 1: Update Playwright commands for workspaces** + +In `apps/web/playwright.config.ts`, make the web server command build from the +workspace root or app directory consistently: + +```ts +command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js` +``` + +This command runs inside `apps/web` when invoked through the app package script. + +- [ ] **Step 2: Update Storybook package paths** + +`packages/ui/.storybook/main.ts` should use `../src/**/*.stories.@(js|ts|tsx)`. +`packages/ui/.storybook/preview.ts` should import `../src/styles.css` and use +generic MSW handlers only if they are moved into the UI package. + +- [ ] **Step 3: Update the container build** + +`apps/web/Containerfile` should build from the repository root context or copy +only the workspace files it needs. Preserve the runtime command: + +```dockerfile +CMD ["bun", "build/index.js"] +``` + +- [ ] **Step 4: Run full root checks** + +Run: + +```sh +bun install +bun run check +bun run test:unit +bun run build +bun run build-storybook +bun run test:e2e +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```sh +git add README.md apps/web packages/ui package.json turbo.json bun.lock +git commit -m "build: wire turbo workspace qa" +``` + +## Task 5: Final QA, PR, Review, Merge, Deploy + +**Files:** +- No planned source edits unless verification finds blockers. + +- [ ] **Step 1: Run release gate** + +Run: + +```sh +bun run test:qa +``` + +Expected: PASS. + +- [ ] **Step 2: Push and open PR** + +Run: + +```sh +git push -u origin codex/turbo-component-library +``` + +Open a ready PR against `main` titled: + +```text +refactor: migrate dashboard to turbo component library +``` + +- [ ] **Step 3: Independent review** + +Send an independent reviewer to inspect the PR diff against the objective: +Turbo repo, `apps/web`, compiled `packages/ui`, reusable components removed +from app, Storybook with UI package, root QA passing, no server behavior change. + +- [ ] **Step 4: Resolve blockers** + +For each blocking review finding, write or update a failing test first, verify +the failure, implement the fix, run targeted checks, commit, push, and re-review. + +- [ ] **Step 5: Merge and deploy** + +When review and checks are clean, merge the PR into `main`, sync the production +checkout, rebuild the Podman image, restart `dimensionlab-website.service`, and +verify `https://dimensionlab.net/` with a browser smoke check. + +## Self-Review + +- Spec coverage: every completion criterion in the design maps to Task 1 through + Task 5. +- Placeholder scan: no task says TBD, TODO, or "add tests" without commands. +- Type consistency: package names are consistently `@dimensionlab/ui` and + `@dimensionlab/web`; the app adapter path is consistently + `$lib/ui-adapter/model-renderer`.