From ae6eabed6f0746a0bad5fa493d07de09dcac6c5e Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:10:16 +0200 Subject: [PATCH 01/43] build(turbo): normalize workspace task graph --- README.md | 7 +- apps/web/package.json | 1 - apps/web/playwright.config.ts | 2 +- apps/web/src/lib/workspace-boundary.test.ts | 53 +++++++++++++- package.json | 24 +++---- turbo.json | 80 +++++++++++++++++---- 6 files changed, 136 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 59dedf6..31c1333 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ This MVP uses Drizzle with Bun SQLite for local file-backed persistence. - `bun run test`: run the unit test stage in all workspaces. - `bun run test:unit`: run Vitest explicitly as the unit test stage. - `bun run test:e2e`: build and run Playwright browser smoke and QA checks. -- `bun run test:qa`: run the release gate through Turbo. +- `bun run test:qa`: run the release gate through Turbo across check, unit, + build, Storybook, and e2e tasks. - `bun run build`: build the UI package, production website, and Bun server. - `bun run preview`: preview the production web build. - `bun run storybook`: start the UI package component explorer on port 6006. @@ -102,7 +103,9 @@ bun run test:qa The gate runs TypeScript checks, Vitest coverage for model, persistence, renderer, datasource mocks, and presentation boundaries, the production build, the static Storybook build, and Playwright desktop/mobile -smoke checks against the built adapter output. Playwright also performs +smoke checks against the built adapter output. Turbo owns the release task +graph; app package scripts stay as leaf commands and do not re-run the QA +pipeline internally. Playwright also performs baseline screenshot checks, keyboard navigation checks, reduced-motion checks, landmark checks, and axe accessibility checks against the real model-driven route. diff --git a/apps/web/package.json b/apps/web/package.json index 7875aa8..c2c183f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,7 +11,6 @@ "test": "vitest run", "test:unit": "vitest run", "test:e2e": "env -u NO_COLOR playwright test", - "test:qa": "bun run check && bun run test:unit && bun run build && bun run test:e2e", "db:generate": "drizzle-kit generate", "db:check": "drizzle-kit check" }, diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index afba557..0dbd949 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ timeout: 120_000, }, { - command: `cd ../.. && bun run build-storybook && STORYBOOK_STATIC_PORT=${storybookPort} bun apps/web/tests/e2e/storybook-server.ts`, + command: `cd ../.. && STORYBOOK_STATIC_PORT=${storybookPort} bun apps/web/tests/e2e/storybook-server.ts`, url: storybookURL, reuseExistingServer: false, timeout: 120_000, diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 66a11c5..5707a8c 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -18,10 +18,61 @@ describe("workspace boundaries", () => { expect(packageJson.private).toBe(true); expect(packageJson.workspaces).toEqual(["apps/*", "packages/*"]); - expect(packageJson.scripts?.build).toBe("turbo build"); + expect(packageJson.scripts?.build).toBe("turbo run build"); expect(existsSync(join(root, "turbo.json"))).toBe(true); }); + test("keeps release orchestration in the root turbo task graph", () => { + const rootPackage = JSON.parse( + readFileSync(join(root, "package.json"), "utf8"), + ) as { + scripts?: Record; + }; + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { scripts?: Record }; + const turboConfig = JSON.parse( + readFileSync(join(root, "turbo.json"), "utf8"), + ) as { + globalDependencies?: string[]; + tasks?: Record; + }; + + expect(rootPackage.scripts?.["test:qa"]).toBe( + "turbo run check test:unit build @dimensionlab/ui#build-storybook @dimensionlab/web#test:e2e", + ); + expect(webPackage.scripts).not.toHaveProperty("test:qa"); + expect(turboConfig.tasks).not.toHaveProperty("test:qa"); + expect(turboConfig.globalDependencies).toEqual( + expect.arrayContaining(["bun.lock", "tsconfig.base.json"]), + ); + expect(turboConfig.tasks?.["test:e2e"]?.env).toEqual( + expect.arrayContaining([ + "CI", + "PLAYWRIGHT_DATABASE_URL", + "PLAYWRIGHT_PORT", + "PLAYWRIGHT_STORYBOOK_PORT", + ]), + ); + }); + + test("lets turbo build Storybook before e2e serves the static artifact", () => { + const playwrightConfig = readFileSync( + join(root, "apps/web/playwright.config.ts"), + "utf8", + ); + const turboConfig = JSON.parse( + readFileSync(join(root, "turbo.json"), "utf8"), + ) as { + tasks?: Record; + }; + + expect(playwrightConfig).not.toContain("bun run build-storybook"); + expect(turboConfig.tasks?.["test:e2e"]?.dependsOn).toEqual( + expect.arrayContaining(["@dimensionlab/ui#build-storybook"]), + ); + }); + test("keeps the website app and reusable UI library as separate packages", () => { const webPackage = JSON.parse( readFileSync(join(root, "apps/web/package.json"), "utf8"), diff --git a/package.json b/package.json index 5b94c8f..e33d3aa 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,18 @@ "packages/*" ], "scripts": { - "dev": "turbo dev --filter=@dimensionlab/web", - "build": "turbo build", - "preview": "bun run --cwd apps/web 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 run --cwd apps/web db:generate", - "db:check": "bun run --cwd apps/web db:check" + "dev": "turbo run dev --filter=@dimensionlab/web", + "build": "turbo run build", + "preview": "turbo run preview --filter=@dimensionlab/web", + "storybook": "turbo run storybook --filter=@dimensionlab/ui", + "build-storybook": "turbo run build-storybook --filter=@dimensionlab/ui", + "check": "turbo run check", + "test": "turbo run test:unit", + "test:unit": "turbo run test:unit", + "test:e2e": "turbo run test:e2e --filter=@dimensionlab/web", + "test:qa": "turbo run check test:unit build @dimensionlab/ui#build-storybook @dimensionlab/web#test:e2e", + "db:generate": "turbo run db:generate --filter=@dimensionlab/web", + "db:check": "turbo run db:check --filter=@dimensionlab/web" }, "devDependencies": { "turbo": "^2.5.0" diff --git a/turbo.json b/turbo.json index ee027a8..446d6fe 100644 --- a/turbo.json +++ b/turbo.json @@ -1,9 +1,16 @@ { "$schema": "https://turbo.build/schema.json", + "globalDependencies": [ + "bun.lock", + "package.json", + "tsconfig.base.json", + "tsconfig.json" + ], "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist/**", "build/**"] + "outputs": ["dist/**", "build/**"], + "env": ["NODE_ENV", "VITE_*"] }, "check": { "dependsOn": ["^build"], @@ -11,33 +18,78 @@ }, "test:unit": { "dependsOn": ["^build"], - "outputs": [] + "outputs": [], + "env": [ + "AGENT_CONFIG_TOKEN", + "DASHBOARD_MIGRATIONS_DIR", + "DATABASE_URL", + "DISABLE_LIVE_DATASOURCES", + "PROMETHEUS_BASE_URL" + ] }, "build-storybook": { "dependsOn": ["^build"], - "outputs": ["storybook-static/**"] + "outputs": ["storybook-static/**"], + "env": ["NODE_ENV", "STORYBOOK_*", "VITE_*"] }, "test:e2e": { - "dependsOn": ["build", "^build"], - "outputs": ["test-results/**", "playwright-report/**"] - }, - "test:qa": { "dependsOn": [ - "check", - "test:unit", "build", - "build-storybook", - "test:e2e" + "^build", + "@dimensionlab/ui#build-storybook" ], - "outputs": [] + "outputs": ["test-results/**", "playwright-report/**"], + "env": [ + "AGENT_CONFIG_TOKEN", + "CI", + "DASHBOARD_MIGRATIONS_DIR", + "DATABASE_URL", + "DISABLE_LIVE_DATASOURCES", + "PLAYWRIGHT_DATABASE_URL", + "PLAYWRIGHT_PORT", + "PLAYWRIGHT_STORYBOOK_PORT", + "PROMETHEUS_BASE_URL", + "STORYBOOK_STATIC_PORT" + ] + }, + "db:generate": { + "cache": false + }, + "db:check": { + "outputs": [], + "env": ["DATABASE_URL"] }, "dev": { "cache": false, - "persistent": true + "persistent": true, + "env": [ + "AGENT_CONFIG_TOKEN", + "DASHBOARD_DEV_API_HOST", + "DASHBOARD_DEV_API_PORT", + "DASHBOARD_DEV_API_TARGET", + "DASHBOARD_MIGRATIONS_DIR", + "DATABASE_URL", + "HOST", + "PORT", + "PROMETHEUS_BASE_URL" + ] + }, + "preview": { + "cache": false, + "persistent": true, + "env": [ + "AGENT_CONFIG_TOKEN", + "DASHBOARD_MIGRATIONS_DIR", + "DATABASE_URL", + "HOST", + "PORT", + "PROMETHEUS_BASE_URL" + ] }, "storybook": { "cache": false, - "persistent": true + "persistent": true, + "env": ["HOST", "PORT", "STORYBOOK_*", "VITE_*"] } } } From c8183bcc474fad99a59930fc3becd3876a04b4f3 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:16:41 +0200 Subject: [PATCH 02/43] fix(turbo): rely on graph build before e2e --- apps/web/playwright.config.ts | 2 +- apps/web/src/lib/workspace-boundary.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 0dbd949..ac0fe5e 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ }, webServer: [ { - 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`, + command: `DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`, url: baseURL, reuseExistingServer: false, timeout: 120_000, diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 5707a8c..51652dc 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -56,7 +56,7 @@ describe("workspace boundaries", () => { ); }); - test("lets turbo build Storybook before e2e serves the static artifact", () => { + test("lets turbo build app and Storybook artifacts before e2e serves them", () => { const playwrightConfig = readFileSync( join(root, "apps/web/playwright.config.ts"), "utf8", @@ -67,6 +67,7 @@ describe("workspace boundaries", () => { tasks?: Record; }; + expect(playwrightConfig).not.toContain("bun run build &&"); expect(playwrightConfig).not.toContain("bun run build-storybook"); expect(turboConfig.tasks?.["test:e2e"]?.dependsOn).toEqual( expect.arrayContaining(["@dimensionlab/ui#build-storybook"]), From 7a6ad8ab0ec9fe806b4aba37261af9bfaed1abdd Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:26:19 +0200 Subject: [PATCH 03/43] refactor(model): extract dashboard model package --- README.md | 9 ++- apps/web/package.json | 1 + .../dimensionlab.test.ts | 2 +- .../dimensionlab.ts | 2 +- apps/web/src/lib/model/fixtures/index.ts | 2 - .../web/src/lib/presentation-boundary.test.ts | 4 +- .../server/agent-config/agent-config.test.ts | 4 +- apps/web/src/lib/server/agent-config/index.ts | 2 +- apps/web/src/lib/server/dashboard.test.ts | 4 +- apps/web/src/lib/server/dashboard.ts | 4 +- .../datasources/dashboard-datasources.test.ts | 2 +- apps/web/src/lib/server/datasources/index.ts | 2 +- .../src/lib/server/db/dashboard-store.test.ts | 4 +- apps/web/src/lib/server/db/dashboard-store.ts | 2 +- .../lib/server/db/model-migrations.test.ts | 4 +- .../web/src/lib/server/db/model-migrations.ts | 2 +- apps/web/src/lib/server/db/schema.ts | 2 +- .../src/lib/ui-adapter/model-renderer.test.ts | 7 ++- apps/web/src/lib/ui-adapter/model-renderer.ts | 2 +- apps/web/src/lib/workspace-boundary.test.ts | 25 ++++++++ apps/web/src/page.test.tsx | 2 +- apps/web/src/server/routes/dashboard.test.ts | 2 +- apps/web/tsconfig.json | 4 ++ apps/web/vite.config.ts | 6 ++ bun.lock | 57 ++++++++++++------- packages/dashboard-model/package.json | 34 +++++++++++ .../dashboard-model/src}/fixtures/generic.ts | 0 .../dashboard-model/src/fixtures/index.ts | 1 + .../dashboard-model/src}/index.ts | 0 .../dashboard-model/src}/schema.test.ts | 7 --- .../dashboard-model/src}/schema.ts | 0 .../dashboard-model/src}/validation.ts | 0 packages/dashboard-model/tsconfig.build.json | 12 ++++ packages/dashboard-model/tsconfig.json | 9 +++ tsconfig.json | 1 + 35 files changed, 164 insertions(+), 57 deletions(-) rename apps/web/src/lib/{model/fixtures => dashboard-seed}/dimensionlab.test.ts (99%) rename apps/web/src/lib/{model/fixtures => dashboard-seed}/dimensionlab.ts (99%) delete mode 100644 apps/web/src/lib/model/fixtures/index.ts create mode 100644 packages/dashboard-model/package.json rename {apps/web/src/lib/model => packages/dashboard-model/src}/fixtures/generic.ts (100%) create mode 100644 packages/dashboard-model/src/fixtures/index.ts rename {apps/web/src/lib/model => packages/dashboard-model/src}/index.ts (100%) rename {apps/web/src/lib/model => packages/dashboard-model/src}/schema.test.ts (96%) rename {apps/web/src/lib/model => packages/dashboard-model/src}/schema.ts (100%) rename {apps/web/src/lib/model => packages/dashboard-model/src}/validation.ts (100%) create mode 100644 packages/dashboard-model/tsconfig.build.json create mode 100644 packages/dashboard-model/tsconfig.json diff --git a/README.md b/README.md index 31c1333..98dc8ec 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,16 @@ reusable React component library. This project is not a Homepage customization and does not depend on Homepage runtime, frontend code, or configuration. The dashboard will be model-driven: -the reusable UI package stays content-free, while environment-specific data -lives in validated dashboard model state inside the web app. +the reusable UI package stays content-free, the reusable dashboard model +package owns schema and validation, and environment-specific data lives in +validated dashboard model state inside the web app. ## Workspace Layout - `apps/web`: Vite React website, Bun API server, model fixtures, Drizzle persistence, Playwright e2e checks, and container build. +- `packages/dashboard-model`: reusable dashboard schema, validation, and + generic model fixtures shared by apps and tooling. - `packages/ui`: reusable dashboard React components, design tokens, shadcn/radix primitives, generic fixtures, and Storybook. - `docs/superpowers`: migration specs and execution plans used for this repo. @@ -65,7 +68,7 @@ with an explicit migration error. ## Seed Data The initial Dimension Lab dashboard lives in -`apps/web/src/lib/model/fixtures/dimensionlab.ts` as validated model data. It +`apps/web/src/lib/dashboard-seed/dimensionlab.ts` as validated model data. It includes the first-screen telemetry, service groups, status strip, weather module, Iconify icon identifiers, links, and datasource references. Values that are not live yet are labeled as fallback values in the data so later datasource diff --git a/apps/web/package.json b/apps/web/package.json index c2c183f..2e9fef6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "db:check": "drizzle-kit check" }, "dependencies": { + "@dimensionlab/dashboard-model": "workspace:*", "@dimensionlab/ui": "workspace:*", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", diff --git a/apps/web/src/lib/model/fixtures/dimensionlab.test.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts similarity index 99% rename from apps/web/src/lib/model/fixtures/dimensionlab.test.ts rename to apps/web/src/lib/dashboard-seed/dimensionlab.test.ts index 38b2ac6..31c01d9 100644 --- a/apps/web/src/lib/model/fixtures/dimensionlab.test.ts +++ b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts @@ -7,7 +7,7 @@ import type { DatasourceReference, ServiceEntry, TelemetryCard, -} from "../schema"; +} from "@dimensionlab/dashboard-model"; describe("Dimension Lab dashboard seed", () => { test("defines the primary first-screen sections as model data", () => { diff --git a/apps/web/src/lib/model/fixtures/dimensionlab.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.ts similarity index 99% rename from apps/web/src/lib/model/fixtures/dimensionlab.ts rename to apps/web/src/lib/dashboard-seed/dimensionlab.ts index c2b4808..6cc544b 100644 --- a/apps/web/src/lib/model/fixtures/dimensionlab.ts +++ b/apps/web/src/lib/dashboard-seed/dimensionlab.ts @@ -7,7 +7,7 @@ import { type ServiceGroup, type Severity, type TelemetryCard, -} from "../schema"; +} from "@dimensionlab/dashboard-model"; type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; diff --git a/apps/web/src/lib/model/fixtures/index.ts b/apps/web/src/lib/model/fixtures/index.ts deleted file mode 100644 index a04278d..0000000 --- a/apps/web/src/lib/model/fixtures/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { dimensionLabDashboardFixture } from "./dimensionlab"; -export { genericDashboardFixture } from "./generic"; diff --git a/apps/web/src/lib/presentation-boundary.test.ts b/apps/web/src/lib/presentation-boundary.test.ts index 7daf204..8a62205 100644 --- a/apps/web/src/lib/presentation-boundary.test.ts +++ b/apps/web/src/lib/presentation-boundary.test.ts @@ -74,5 +74,7 @@ function findFiles(path: string, extension: string): string[] { } function withoutInternalPackageScope(source: string): string { - return source.replaceAll("@dimensionlab/ui", "@internal/ui"); + return source + .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model") + .replaceAll("@dimensionlab/ui", "@internal/ui"); } diff --git a/apps/web/src/lib/server/agent-config/agent-config.test.ts b/apps/web/src/lib/server/agent-config/agent-config.test.ts index 87449e8..cdc60cf 100644 --- a/apps/web/src/lib/server/agent-config/agent-config.test.ts +++ b/apps/web/src/lib/server/agent-config/agent-config.test.ts @@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test } from "vitest"; -import type { DashboardDocument } from "$lib/model"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store"; import { AgentConfigAuthorizationError, diff --git a/apps/web/src/lib/server/agent-config/index.ts b/apps/web/src/lib/server/agent-config/index.ts index ffa7dd8..ab3d0ae 100644 --- a/apps/web/src/lib/server/agent-config/index.ts +++ b/apps/web/src/lib/server/agent-config/index.ts @@ -16,7 +16,7 @@ import { type ServiceGroup, type StatusItem, type TelemetryCard, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import { createDashboardStore, DashboardRevisionNotFoundError, diff --git a/apps/web/src/lib/server/dashboard.test.ts b/apps/web/src/lib/server/dashboard.test.ts index b15fa6c..7851ebb 100644 --- a/apps/web/src/lib/server/dashboard.test.ts +++ b/apps/web/src/lib/server/dashboard.test.ts @@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test } from "vitest"; -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { createDashboardStore, type DashboardStore } from "./db/dashboard-store"; import { loadDashboardRuntime } from "./dashboard"; diff --git a/apps/web/src/lib/server/dashboard.ts b/apps/web/src/lib/server/dashboard.ts index 7821ee0..47ebfa2 100644 --- a/apps/web/src/lib/server/dashboard.ts +++ b/apps/web/src/lib/server/dashboard.ts @@ -1,5 +1,5 @@ -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; -import type { DashboardDocument } from "$lib/model"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { createDashboardStore, DashboardPersistenceValidationError, diff --git a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts index 19082a2..61acdd3 100644 --- a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts +++ b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest"; import { DASHBOARD_SCHEMA_VERSION, type DashboardDocument, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import { resolveDashboardDatasources } from "."; describe("dashboard datasource resolution", () => { diff --git a/apps/web/src/lib/server/datasources/index.ts b/apps/web/src/lib/server/datasources/index.ts index 804fbb5..d69db86 100644 --- a/apps/web/src/lib/server/datasources/index.ts +++ b/apps/web/src/lib/server/datasources/index.ts @@ -8,7 +8,7 @@ import type { StatusItem, StatusStrip, TelemetryCard, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; export interface DatasourceResolutionOptions { fetch?: DatasourceFetch; diff --git a/apps/web/src/lib/server/db/dashboard-store.test.ts b/apps/web/src/lib/server/db/dashboard-store.test.ts index ecb80f3..c2717f5 100644 --- a/apps/web/src/lib/server/db/dashboard-store.test.ts +++ b/apps/web/src/lib/server/db/dashboard-store.test.ts @@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; -import type { DashboardDocument } from "$lib/model"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { DashboardPersistenceValidationError, createDashboardStore, diff --git a/apps/web/src/lib/server/db/dashboard-store.ts b/apps/web/src/lib/server/db/dashboard-store.ts index 92c7a73..3928206 100644 --- a/apps/web/src/lib/server/db/dashboard-store.ts +++ b/apps/web/src/lib/server/db/dashboard-store.ts @@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm"; import { type DashboardDocument, type DashboardValidationFailure, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import { type DashboardDatabaseConnection, openDashboardDatabase, diff --git a/apps/web/src/lib/server/db/model-migrations.test.ts b/apps/web/src/lib/server/db/model-migrations.test.ts index eb16b8f..5c863a0 100644 --- a/apps/web/src/lib/server/db/model-migrations.test.ts +++ b/apps/web/src/lib/server/db/model-migrations.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import { DASHBOARD_SCHEMA_VERSION } from "$lib/model"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { DASHBOARD_SCHEMA_VERSION } from "@dimensionlab/dashboard-model"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { UnsupportedDashboardModelVersionError, migrateDashboardDocumentForPersistence, diff --git a/apps/web/src/lib/server/db/model-migrations.ts b/apps/web/src/lib/server/db/model-migrations.ts index 92550e6..72e9c47 100644 --- a/apps/web/src/lib/server/db/model-migrations.ts +++ b/apps/web/src/lib/server/db/model-migrations.ts @@ -3,7 +3,7 @@ import { validateDashboardDocument, type DashboardDocument, type DashboardValidationFailure, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; export interface DashboardModelMigrationSuccess { valid: true; diff --git a/apps/web/src/lib/server/db/schema.ts b/apps/web/src/lib/server/db/schema.ts index c148035..a3a7206 100644 --- a/apps/web/src/lib/server/db/schema.ts +++ b/apps/web/src/lib/server/db/schema.ts @@ -1,4 +1,4 @@ -import type { DashboardDocument } from "$lib/model"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const dashboardDocuments = sqliteTable("dashboard_documents", { diff --git a/apps/web/src/lib/ui-adapter/model-renderer.test.ts b/apps/web/src/lib/ui-adapter/model-renderer.test.ts index fb04ec4..2f8178d 100644 --- a/apps/web/src/lib/ui-adapter/model-renderer.test.ts +++ b/apps/web/src/lib/ui-adapter/model-renderer.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { type DashboardDocument } from "$lib/model"; -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { type DashboardDocument } from "@dimensionlab/dashboard-model"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { dashboardDocumentToUiDashboard } from "./model-renderer"; const appRoot = process.cwd().endsWith(`${join("apps", "web")}`) @@ -93,6 +93,7 @@ describe("dashboard model renderer", () => { "utf8", ) .toLowerCase() + .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model") .replaceAll("@dimensionlab/ui", "@internal/ui"); expect(source).not.toContain("dimension"); diff --git a/apps/web/src/lib/ui-adapter/model-renderer.ts b/apps/web/src/lib/ui-adapter/model-renderer.ts index 1db2173..7ac1912 100644 --- a/apps/web/src/lib/ui-adapter/model-renderer.ts +++ b/apps/web/src/lib/ui-adapter/model-renderer.ts @@ -6,7 +6,7 @@ import type { StatusItem, StatusStrip, TelemetryCard, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import type { UiDashboardPreview, UiModuleBlock, diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 51652dc..4edcfc6 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -88,4 +88,29 @@ describe("workspace boundaries", () => { expect(uiPackage.exports).toHaveProperty("."); expect(uiPackage.exports).toHaveProperty("./styles.css"); }); + + test("keeps the dashboard model in a reusable internal package", () => { + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { dependencies?: Record }; + const modelPackage = JSON.parse( + readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"), + ) as { exports?: Record; name?: string }; + const tsconfig = JSON.parse( + readFileSync(join(root, "tsconfig.json"), "utf8"), + ) as { references?: Array<{ path: string }> }; + + expect(webPackage.dependencies?.["@dimensionlab/dashboard-model"]).toBe( + "workspace:*", + ); + expect(modelPackage.name).toBe("@dimensionlab/dashboard-model"); + expect(modelPackage.exports).toHaveProperty("."); + expect(modelPackage.exports).toHaveProperty("./fixtures"); + expect(tsconfig.references).toEqual( + expect.arrayContaining([{ path: "./packages/dashboard-model" }]), + ); + expect(existsSync(join(root, "apps/web/src/lib/model/index.ts"))).toBe( + false, + ); + }); }); diff --git a/apps/web/src/page.test.tsx b/apps/web/src/page.test.tsx index fc68147..2ec5869 100644 --- a/apps/web/src/page.test.tsx +++ b/apps/web/src/page.test.tsx @@ -1,6 +1,6 @@ import { renderToString } from "react-dom/server"; import { describe, expect, test } from "vitest"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { AppStateView, resolveDocumentMetadata } from "./App"; describe("home page model renderer", () => { diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts index ebe575b..21ad58d 100644 --- a/apps/web/src/server/routes/dashboard.test.ts +++ b/apps/web/src/server/routes/dashboard.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; import { loadDashboardResponse } from "./dashboard"; describe("dashboard API route", () => { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index edfa07d..f9f784d 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "baseUrl": ".", "paths": { + "@dimensionlab/dashboard-model": ["../../packages/dashboard-model/src/index.ts"], + "@dimensionlab/dashboard-model/fixtures": [ + "../../packages/dashboard-model/src/fixtures/index.ts" + ], "@dimensionlab/ui": ["../../packages/ui/src/index.ts"], "@dimensionlab/ui/styles.css": ["../../packages/ui/src/styles.css"], "$lib/*": ["src/lib/*"] diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index d88ee57..687c180 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -30,6 +30,12 @@ export default defineConfig({ "@dimensionlab/ui": fileURLToPath( new URL("../../packages/ui/src/index.ts", import.meta.url), ), + "@dimensionlab/dashboard-model/fixtures": fileURLToPath( + new URL("../../packages/dashboard-model/src/fixtures/index.ts", import.meta.url), + ), + "@dimensionlab/dashboard-model": fileURLToPath( + new URL("../../packages/dashboard-model/src/index.ts", import.meta.url), + ), $lib: fileURLToPath(new URL("./src/lib", import.meta.url)), }, }, diff --git a/bun.lock b/bun.lock index 543520d..d6915d0 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "name": "@dimensionlab/web", "version": "0.0.1", "dependencies": { + "@dimensionlab/dashboard-model": "workspace:*", "@dimensionlab/ui": "workspace:*", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", @@ -40,6 +41,22 @@ "vitest": "^4.1.9", }, }, + "packages/dashboard-model": { + "name": "@dimensionlab/dashboard-model", + "version": "0.0.1", + "dependencies": { + "@sinclair/typebox": "^0.34.49", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "@types/node": "^25.9.3", + "bun-types": "^1.3.14", + "typescript": "^6.0.3", + "vitest": "^4.1.9", + }, + }, "packages/ui": { "name": "@dimensionlab/ui", "version": "0.0.1", @@ -137,6 +154,8 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@dimensionlab/dashboard-model": ["@dimensionlab/dashboard-model@workspace:packages/dashboard-model"], + "@dimensionlab/ui": ["@dimensionlab/ui@workspace:packages/ui"], "@dimensionlab/web": ["@dimensionlab/web@workspace:apps/web"], @@ -671,7 +690,7 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], @@ -681,7 +700,7 @@ "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], @@ -745,7 +764,7 @@ "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -873,7 +892,7 @@ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1479,6 +1498,8 @@ "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], @@ -1495,14 +1516,6 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1549,14 +1562,14 @@ "shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], - - "vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1625,8 +1638,6 @@ "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1689,10 +1700,16 @@ "shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "storybook/@vitest/expect/chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "storybook/@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], } } diff --git a/packages/dashboard-model/package.json b/packages/dashboard-model/package.json new file mode 100644 index 0000000..978522e --- /dev/null +++ b/packages/dashboard-model/package.json @@ -0,0 +1,34 @@ +{ + "name": "@dimensionlab/dashboard-model", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./fixtures": { + "types": "./dist/fixtures/index.d.ts", + "default": "./dist/fixtures/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "check": "tsc --noEmit", + "test": "vitest run", + "test:unit": "vitest run" + }, + "dependencies": { + "@sinclair/typebox": "^0.34.49", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "@types/node": "^25.9.3", + "bun-types": "^1.3.14", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + } +} diff --git a/apps/web/src/lib/model/fixtures/generic.ts b/packages/dashboard-model/src/fixtures/generic.ts similarity index 100% rename from apps/web/src/lib/model/fixtures/generic.ts rename to packages/dashboard-model/src/fixtures/generic.ts diff --git a/packages/dashboard-model/src/fixtures/index.ts b/packages/dashboard-model/src/fixtures/index.ts new file mode 100644 index 0000000..f62d646 --- /dev/null +++ b/packages/dashboard-model/src/fixtures/index.ts @@ -0,0 +1 @@ +export { genericDashboardFixture } from "./generic"; diff --git a/apps/web/src/lib/model/index.ts b/packages/dashboard-model/src/index.ts similarity index 100% rename from apps/web/src/lib/model/index.ts rename to packages/dashboard-model/src/index.ts diff --git a/apps/web/src/lib/model/schema.test.ts b/packages/dashboard-model/src/schema.test.ts similarity index 96% rename from apps/web/src/lib/model/schema.test.ts rename to packages/dashboard-model/src/schema.test.ts index 7eeed74..9975f87 100644 --- a/apps/web/src/lib/model/schema.test.ts +++ b/packages/dashboard-model/src/schema.test.ts @@ -5,7 +5,6 @@ import { validateDashboardDocument, } from "."; import { - dimensionLabDashboardFixture, genericDashboardFixture, } from "./fixtures"; @@ -19,12 +18,6 @@ describe("dashboard model validation", () => { } }); - it("accepts the Dimension Lab dashboard fixture", () => { - const result = validateDashboardDocument(dimensionLabDashboardFixture); - - expect(result.valid).toBe(true); - }); - it("rejects documents with an unsupported schema version", () => { const invalid = { ...genericDashboardFixture, diff --git a/apps/web/src/lib/model/schema.ts b/packages/dashboard-model/src/schema.ts similarity index 100% rename from apps/web/src/lib/model/schema.ts rename to packages/dashboard-model/src/schema.ts diff --git a/apps/web/src/lib/model/validation.ts b/packages/dashboard-model/src/validation.ts similarity index 100% rename from apps/web/src/lib/model/validation.ts rename to packages/dashboard-model/src/validation.ts diff --git a/packages/dashboard-model/tsconfig.build.json b/packages/dashboard-model/tsconfig.build.json new file mode 100644 index 0000000..5d3e7ce --- /dev/null +++ b/packages/dashboard-model/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": false, + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "src/**/*.test.ts"] +} diff --git a/packages/dashboard-model/tsconfig.json b/packages/dashboard-model/tsconfig.json new file mode 100644 index 0000000..26cb1c4 --- /dev/null +++ b/packages/dashboard-model/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "types": ["node", "bun-types"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/tsconfig.json b/tsconfig.json index 57ef1e6..8fe9b00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ + { "path": "./packages/dashboard-model" }, { "path": "./apps/web" }, { "path": "./packages/ui" } ] From 9311b81102480516c59e2e9dab631ee611a1737b Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:34:31 +0200 Subject: [PATCH 04/43] fix(container): stage model package manifest --- apps/web/Containerfile | 1 + apps/web/src/lib/workspace-boundary.test.ts | 32 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/apps/web/Containerfile b/apps/web/Containerfile index 912f780..7f12f94 100644 --- a/apps/web/Containerfile +++ b/apps/web/Containerfile @@ -4,6 +4,7 @@ WORKDIR /repo ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH COPY package.json bun.lock turbo.json tsconfig.base.json ./ COPY apps/web/package.json apps/web/package.json +COPY packages/dashboard-model/package.json packages/dashboard-model/package.json COPY packages/ui/package.json packages/ui/package.json RUN bun install --frozen-lockfile diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 4edcfc6..4f4200c 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -113,4 +113,36 @@ describe("workspace boundaries", () => { false, ); }); + + test("stages web workspace dependency manifests before container install", () => { + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { dependencies?: Record }; + const workspacePackages = [ + "packages/ui/package.json", + "packages/dashboard-model/package.json", + ].map((manifestPath) => { + const packageJson = JSON.parse( + readFileSync(join(root, manifestPath), "utf8"), + ) as { name?: string }; + + return [packageJson.name, manifestPath] as const; + }); + const manifestsByPackageName = new Map(workspacePackages); + const containerfile = readFileSync( + join(root, "apps/web/Containerfile"), + "utf8", + ); + + const workspaceDependencyManifests = Object.entries( + webPackage.dependencies ?? {}, + ) + .filter(([, version]) => version.startsWith("workspace:")) + .map(([packageName]) => manifestsByPackageName.get(packageName)); + + expect(workspaceDependencyManifests).not.toContain(undefined); + for (const manifestPath of workspaceDependencyManifests) { + expect(containerfile).toContain(`COPY ${manifestPath} ${manifestPath}`); + } + }); }); From 32ad2cebe474535a44841f128398b87a4a4b2e44 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:43:09 +0200 Subject: [PATCH 05/43] refactor(ui): organize component source tree --- README.md | 3 +- .../src/components/{ => foundation}/Badge.tsx | 2 +- .../components/{ => foundation}/Button.tsx | 0 .../{ => foundation}/IconButton.tsx | 0 .../components/{ => foundation}/IconGlyph.tsx | 0 .../{ => foundation}/ProgressMeter.tsx | 4 +- .../components/{ => foundation}/Separator.tsx | 0 .../{ => foundation}/StatusBadge.tsx | 2 +- .../{ => foundation}/ThemeToggle.tsx | 4 +- .../{ => frames}/CornerBracketFrame.tsx | 0 .../{ => frames}/DashboardFrame.tsx | 8 +- .../{ => frames}/DashboardHeader.tsx | 2 +- .../{ => frames}/DiagonalStripeField.tsx | 0 .../components/{ => frames}/FooterCell.tsx | 2 +- .../{ => frames}/FooterStatusCell.tsx | 2 +- .../src/components/{ => frames}/GridFrame.tsx | 0 .../components/{ => frames}/ModuleCard.tsx | 4 +- .../ui/src/components/{ => frames}/Panel.tsx | 0 .../components/{ => frames}/ScanlineField.tsx | 0 .../{ => operations}/ServiceGroupPanel.tsx | 2 +- .../{ => operations}/ServicePanel.tsx | 4 +- .../{ => operations}/ServiceRow.tsx | 6 +- .../{ => operations}/StatusStrip.tsx | 4 +- .../{ => operations}/SystemState.tsx | 4 +- .../{ => operations}/WeatherModule.tsx | 4 +- packages/ui/src/components/render.test.tsx | 20 ++--- .../components/{ => telemetry}/LineChart.tsx | 2 +- .../{ => telemetry}/SignalTrace.tsx | 0 .../components/{ => telemetry}/Sparkline.tsx | 2 +- .../{ => telemetry}/TelemetryCard.tsx | 6 +- .../{ => telemetry}/TelemetryGrid.tsx | 2 +- .../{ => telemetry}/TelemetryStrip.tsx | 2 +- packages/ui/src/content-boundary.test.ts | 25 ++++-- packages/ui/src/index.ts | 60 +++++++------- packages/ui/src/stories/Badge.stories.tsx | 2 +- packages/ui/src/stories/Button.stories.tsx | 2 +- .../stories/CornerBracketFrame.stories.tsx | 2 +- .../ui/src/stories/DashboardFrame.stories.tsx | 2 +- .../src/stories/DashboardHeader.stories.tsx | 2 +- .../src/stories/DashboardOnePager.stories.tsx | 2 +- .../stories/DiagonalStripeField.stories.tsx | 2 +- .../ui/src/stories/FooterCell.stories.tsx | 2 +- .../src/stories/FooterStatusCell.stories.tsx | 2 +- packages/ui/src/stories/GridFrame.stories.tsx | 3 +- .../ui/src/stories/IconButton.stories.tsx | 2 +- packages/ui/src/stories/IconGlyph.stories.tsx | 2 +- packages/ui/src/stories/LineChart.stories.tsx | 2 +- .../ui/src/stories/ModuleCard.stories.tsx | 2 +- packages/ui/src/stories/Panel.stories.tsx | 3 +- .../ui/src/stories/ProgressMeter.stories.tsx | 2 +- .../ui/src/stories/ScanlineField.stories.tsx | 2 +- packages/ui/src/stories/Separator.stories.tsx | 2 +- .../src/stories/ServiceGroupPanel.stories.tsx | 2 +- .../ui/src/stories/ServicePanel.stories.tsx | 2 +- .../ui/src/stories/ServiceRow.stories.tsx | 2 +- .../ui/src/stories/SignalTrace.stories.tsx | 2 +- packages/ui/src/stories/Sparkline.stories.tsx | 2 +- .../ui/src/stories/StatusBadge.stories.tsx | 2 +- .../ui/src/stories/StatusStrip.stories.tsx | 2 +- .../ui/src/stories/SystemState.stories.tsx | 2 +- .../ui/src/stories/TelemetryCard.stories.tsx | 2 +- .../ui/src/stories/TelemetryGrid.stories.tsx | 2 +- .../ui/src/stories/TelemetryStrip.stories.tsx | 2 +- .../ui/src/stories/ThemeToggle.stories.tsx | 2 +- .../ui/src/stories/WeatherModule.stories.tsx | 2 +- packages/ui/src/storybook.test.ts | 78 ++++++++++++++++--- 66 files changed, 194 insertions(+), 124 deletions(-) rename packages/ui/src/components/{ => foundation}/Badge.tsx (84%) rename packages/ui/src/components/{ => foundation}/Button.tsx (100%) rename packages/ui/src/components/{ => foundation}/IconButton.tsx (100%) rename packages/ui/src/components/{ => foundation}/IconGlyph.tsx (100%) rename packages/ui/src/components/{ => foundation}/ProgressMeter.tsx (88%) rename packages/ui/src/components/{ => foundation}/Separator.tsx (100%) rename packages/ui/src/components/{ => foundation}/StatusBadge.tsx (85%) rename packages/ui/src/components/{ => foundation}/ThemeToggle.tsx (92%) rename packages/ui/src/components/{ => frames}/CornerBracketFrame.tsx (100%) rename packages/ui/src/components/{ => frames}/DashboardFrame.tsx (88%) rename packages/ui/src/components/{ => frames}/DashboardHeader.tsx (92%) rename packages/ui/src/components/{ => frames}/DiagonalStripeField.tsx (100%) rename packages/ui/src/components/{ => frames}/FooterCell.tsx (94%) rename packages/ui/src/components/{ => frames}/FooterStatusCell.tsx (75%) rename packages/ui/src/components/{ => frames}/GridFrame.tsx (100%) rename packages/ui/src/components/{ => frames}/ModuleCard.tsx (89%) rename packages/ui/src/components/{ => frames}/Panel.tsx (100%) rename packages/ui/src/components/{ => frames}/ScanlineField.tsx (100%) rename packages/ui/src/components/{ => operations}/ServiceGroupPanel.tsx (76%) rename packages/ui/src/components/{ => operations}/ServicePanel.tsx (87%) rename packages/ui/src/components/{ => operations}/ServiceRow.tsx (89%) rename packages/ui/src/components/{ => operations}/StatusStrip.tsx (82%) rename packages/ui/src/components/{ => operations}/SystemState.tsx (82%) rename packages/ui/src/components/{ => operations}/WeatherModule.tsx (53%) rename packages/ui/src/components/{ => telemetry}/LineChart.tsx (98%) rename packages/ui/src/components/{ => telemetry}/SignalTrace.tsx (100%) rename packages/ui/src/components/{ => telemetry}/Sparkline.tsx (94%) rename packages/ui/src/components/{ => telemetry}/TelemetryCard.tsx (91%) rename packages/ui/src/components/{ => telemetry}/TelemetryGrid.tsx (94%) rename packages/ui/src/components/{ => telemetry}/TelemetryStrip.tsx (76%) diff --git a/README.md b/README.md index 98dc8ec..572f9bc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ validated dashboard model state inside the web app. - `packages/dashboard-model`: reusable dashboard schema, validation, and generic model fixtures shared by apps and tooling. - `packages/ui`: reusable dashboard React components, design tokens, - shadcn/radix primitives, generic fixtures, and Storybook. + shadcn/radix primitives, generic fixtures, and Storybook. Component source is + grouped under `foundation`, `frames`, `operations`, and `telemetry` domains. - `docs/superpowers`: migration specs and execution plans used for this repo. ## Development diff --git a/packages/ui/src/components/Badge.tsx b/packages/ui/src/components/foundation/Badge.tsx similarity index 84% rename from packages/ui/src/components/Badge.tsx rename to packages/ui/src/components/foundation/Badge.tsx index a21ade8..5bdee48 100644 --- a/packages/ui/src/components/Badge.tsx +++ b/packages/ui/src/components/foundation/Badge.tsx @@ -1,4 +1,4 @@ -import type { UiSeverity } from "../types"; +import type { UiSeverity } from "../../types"; import { StatusBadge } from "./StatusBadge"; export interface BadgeProps { diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/foundation/Button.tsx similarity index 100% rename from packages/ui/src/components/Button.tsx rename to packages/ui/src/components/foundation/Button.tsx diff --git a/packages/ui/src/components/IconButton.tsx b/packages/ui/src/components/foundation/IconButton.tsx similarity index 100% rename from packages/ui/src/components/IconButton.tsx rename to packages/ui/src/components/foundation/IconButton.tsx diff --git a/packages/ui/src/components/IconGlyph.tsx b/packages/ui/src/components/foundation/IconGlyph.tsx similarity index 100% rename from packages/ui/src/components/IconGlyph.tsx rename to packages/ui/src/components/foundation/IconGlyph.tsx diff --git a/packages/ui/src/components/ProgressMeter.tsx b/packages/ui/src/components/foundation/ProgressMeter.tsx similarity index 88% rename from packages/ui/src/components/ProgressMeter.tsx rename to packages/ui/src/components/foundation/ProgressMeter.tsx index c7278bf..c3f3aad 100644 --- a/packages/ui/src/components/ProgressMeter.tsx +++ b/packages/ui/src/components/foundation/ProgressMeter.tsx @@ -1,6 +1,6 @@ import type { CSSProperties } from "react"; -import { clampPercent } from "../format"; -import type { UiSeverity } from "../types"; +import { clampPercent } from "../../format"; +import type { UiSeverity } from "../../types"; export interface ProgressMeterProps { value?: number; diff --git a/packages/ui/src/components/Separator.tsx b/packages/ui/src/components/foundation/Separator.tsx similarity index 100% rename from packages/ui/src/components/Separator.tsx rename to packages/ui/src/components/foundation/Separator.tsx diff --git a/packages/ui/src/components/StatusBadge.tsx b/packages/ui/src/components/foundation/StatusBadge.tsx similarity index 85% rename from packages/ui/src/components/StatusBadge.tsx rename to packages/ui/src/components/foundation/StatusBadge.tsx index fb63a04..d2b01b3 100644 --- a/packages/ui/src/components/StatusBadge.tsx +++ b/packages/ui/src/components/foundation/StatusBadge.tsx @@ -1,4 +1,4 @@ -import type { UiSeverity } from "../types"; +import type { UiSeverity } from "../../types"; export interface StatusBadgeProps { label: string; diff --git a/packages/ui/src/components/ThemeToggle.tsx b/packages/ui/src/components/foundation/ThemeToggle.tsx similarity index 92% rename from packages/ui/src/components/ThemeToggle.tsx rename to packages/ui/src/components/foundation/ThemeToggle.tsx index 9e883af..5d83031 100644 --- a/packages/ui/src/components/ThemeToggle.tsx +++ b/packages/ui/src/components/foundation/ThemeToggle.tsx @@ -1,5 +1,5 @@ -import type { UiTheme } from "../theme"; -import { getNextUiTheme } from "../theme"; +import type { UiTheme } from "../../theme"; +import { getNextUiTheme } from "../../theme"; import { IconGlyph } from "./IconGlyph"; export interface ThemeToggleProps { diff --git a/packages/ui/src/components/CornerBracketFrame.tsx b/packages/ui/src/components/frames/CornerBracketFrame.tsx similarity index 100% rename from packages/ui/src/components/CornerBracketFrame.tsx rename to packages/ui/src/components/frames/CornerBracketFrame.tsx diff --git a/packages/ui/src/components/DashboardFrame.tsx b/packages/ui/src/components/frames/DashboardFrame.tsx similarity index 88% rename from packages/ui/src/components/DashboardFrame.tsx rename to packages/ui/src/components/frames/DashboardFrame.tsx index cdf312a..08029cf 100644 --- a/packages/ui/src/components/DashboardFrame.tsx +++ b/packages/ui/src/components/frames/DashboardFrame.tsx @@ -1,10 +1,10 @@ import { useId } from "react"; import type { ReactNode } from "react"; -import type { UiDashboardPreview } from "../types"; +import type { UiDashboardPreview } from "../../types"; import { ModuleCard } from "./ModuleCard"; -import { ServicePanel } from "./ServicePanel"; -import { StatusStrip } from "./StatusStrip"; -import { TelemetryGrid } from "./TelemetryGrid"; +import { ServicePanel } from "../operations/ServicePanel"; +import { StatusStrip } from "../operations/StatusStrip"; +import { TelemetryGrid } from "../telemetry/TelemetryGrid"; export interface DashboardFrameProps { actions?: ReactNode; diff --git a/packages/ui/src/components/DashboardHeader.tsx b/packages/ui/src/components/frames/DashboardHeader.tsx similarity index 92% rename from packages/ui/src/components/DashboardHeader.tsx rename to packages/ui/src/components/frames/DashboardHeader.tsx index 68b4e11..3f418fa 100644 --- a/packages/ui/src/components/DashboardHeader.tsx +++ b/packages/ui/src/components/frames/DashboardHeader.tsx @@ -1,5 +1,5 @@ import { useId } from "react"; -import type { UiModuleBlock } from "../types"; +import type { UiModuleBlock } from "../../types"; import { ModuleCard } from "./ModuleCard"; export interface DashboardHeaderProps { diff --git a/packages/ui/src/components/DiagonalStripeField.tsx b/packages/ui/src/components/frames/DiagonalStripeField.tsx similarity index 100% rename from packages/ui/src/components/DiagonalStripeField.tsx rename to packages/ui/src/components/frames/DiagonalStripeField.tsx diff --git a/packages/ui/src/components/FooterCell.tsx b/packages/ui/src/components/frames/FooterCell.tsx similarity index 94% rename from packages/ui/src/components/FooterCell.tsx rename to packages/ui/src/components/frames/FooterCell.tsx index 760e853..8b9a86d 100644 --- a/packages/ui/src/components/FooterCell.tsx +++ b/packages/ui/src/components/frames/FooterCell.tsx @@ -1,4 +1,4 @@ -import type { UiStatusItem } from "../types"; +import type { UiStatusItem } from "../../types"; export interface FooterCellProps { item: UiStatusItem; diff --git a/packages/ui/src/components/FooterStatusCell.tsx b/packages/ui/src/components/frames/FooterStatusCell.tsx similarity index 75% rename from packages/ui/src/components/FooterStatusCell.tsx rename to packages/ui/src/components/frames/FooterStatusCell.tsx index dcfb8b4..b5b339e 100644 --- a/packages/ui/src/components/FooterStatusCell.tsx +++ b/packages/ui/src/components/frames/FooterStatusCell.tsx @@ -1,4 +1,4 @@ -import type { UiStatusItem } from "../types"; +import type { UiStatusItem } from "../../types"; import { FooterCell } from "./FooterCell"; export function FooterStatusCell({ item }: { item: UiStatusItem }) { diff --git a/packages/ui/src/components/GridFrame.tsx b/packages/ui/src/components/frames/GridFrame.tsx similarity index 100% rename from packages/ui/src/components/GridFrame.tsx rename to packages/ui/src/components/frames/GridFrame.tsx diff --git a/packages/ui/src/components/ModuleCard.tsx b/packages/ui/src/components/frames/ModuleCard.tsx similarity index 89% rename from packages/ui/src/components/ModuleCard.tsx rename to packages/ui/src/components/frames/ModuleCard.tsx index d05bee0..9b96df9 100644 --- a/packages/ui/src/components/ModuleCard.tsx +++ b/packages/ui/src/components/frames/ModuleCard.tsx @@ -1,6 +1,6 @@ import { useId } from "react"; -import type { UiModuleBlock } from "../types"; -import { IconGlyph } from "./IconGlyph"; +import type { UiModuleBlock } from "../../types"; +import { IconGlyph } from "../foundation/IconGlyph"; export interface ModuleCardProps { module: UiModuleBlock; diff --git a/packages/ui/src/components/Panel.tsx b/packages/ui/src/components/frames/Panel.tsx similarity index 100% rename from packages/ui/src/components/Panel.tsx rename to packages/ui/src/components/frames/Panel.tsx diff --git a/packages/ui/src/components/ScanlineField.tsx b/packages/ui/src/components/frames/ScanlineField.tsx similarity index 100% rename from packages/ui/src/components/ScanlineField.tsx rename to packages/ui/src/components/frames/ScanlineField.tsx diff --git a/packages/ui/src/components/ServiceGroupPanel.tsx b/packages/ui/src/components/operations/ServiceGroupPanel.tsx similarity index 76% rename from packages/ui/src/components/ServiceGroupPanel.tsx rename to packages/ui/src/components/operations/ServiceGroupPanel.tsx index 4252cc9..9b53340 100644 --- a/packages/ui/src/components/ServiceGroupPanel.tsx +++ b/packages/ui/src/components/operations/ServiceGroupPanel.tsx @@ -1,4 +1,4 @@ -import type { UiServiceGroup } from "../types"; +import type { UiServiceGroup } from "../../types"; import { ServicePanel } from "./ServicePanel"; export function ServiceGroupPanel({ group }: { group: UiServiceGroup }) { diff --git a/packages/ui/src/components/ServicePanel.tsx b/packages/ui/src/components/operations/ServicePanel.tsx similarity index 87% rename from packages/ui/src/components/ServicePanel.tsx rename to packages/ui/src/components/operations/ServicePanel.tsx index 12a2e74..c8b85af 100644 --- a/packages/ui/src/components/ServicePanel.tsx +++ b/packages/ui/src/components/operations/ServicePanel.tsx @@ -1,5 +1,5 @@ -import type { UiServiceGroup } from "../types"; -import { Panel } from "./Panel"; +import type { UiServiceGroup } from "../../types"; +import { Panel } from "../frames/Panel"; import { ServiceRow } from "./ServiceRow"; import { StatusStrip } from "./StatusStrip"; diff --git a/packages/ui/src/components/ServiceRow.tsx b/packages/ui/src/components/operations/ServiceRow.tsx similarity index 89% rename from packages/ui/src/components/ServiceRow.tsx rename to packages/ui/src/components/operations/ServiceRow.tsx index cf065f4..e6e5aa9 100644 --- a/packages/ui/src/components/ServiceRow.tsx +++ b/packages/ui/src/components/operations/ServiceRow.tsx @@ -1,6 +1,6 @@ -import type { UiServiceRow } from "../types"; -import { IconGlyph } from "./IconGlyph"; -import { StatusBadge } from "./StatusBadge"; +import type { UiServiceRow } from "../../types"; +import { IconGlyph } from "../foundation/IconGlyph"; +import { StatusBadge } from "../foundation/StatusBadge"; export interface ServiceRowProps { service: UiServiceRow; diff --git a/packages/ui/src/components/StatusStrip.tsx b/packages/ui/src/components/operations/StatusStrip.tsx similarity index 82% rename from packages/ui/src/components/StatusStrip.tsx rename to packages/ui/src/components/operations/StatusStrip.tsx index 66c6451..3f2bca2 100644 --- a/packages/ui/src/components/StatusStrip.tsx +++ b/packages/ui/src/components/operations/StatusStrip.tsx @@ -1,5 +1,5 @@ -import type { UiStatusItem } from "../types"; -import { FooterCell } from "./FooterCell"; +import type { UiStatusItem } from "../../types"; +import { FooterCell } from "../frames/FooterCell"; export interface StatusStripProps { id?: string; diff --git a/packages/ui/src/components/SystemState.tsx b/packages/ui/src/components/operations/SystemState.tsx similarity index 82% rename from packages/ui/src/components/SystemState.tsx rename to packages/ui/src/components/operations/SystemState.tsx index e817e2b..438fa52 100644 --- a/packages/ui/src/components/SystemState.tsx +++ b/packages/ui/src/components/operations/SystemState.tsx @@ -1,5 +1,5 @@ -import type { UiSeverity } from "../types"; -import { IconGlyph } from "./IconGlyph"; +import type { UiSeverity } from "../../types"; +import { IconGlyph } from "../foundation/IconGlyph"; export interface SystemStateProps { title: string; diff --git a/packages/ui/src/components/WeatherModule.tsx b/packages/ui/src/components/operations/WeatherModule.tsx similarity index 53% rename from packages/ui/src/components/WeatherModule.tsx rename to packages/ui/src/components/operations/WeatherModule.tsx index 0967cfb..c7e55db 100644 --- a/packages/ui/src/components/WeatherModule.tsx +++ b/packages/ui/src/components/operations/WeatherModule.tsx @@ -1,5 +1,5 @@ -import type { UiModuleBlock } from "../types"; -import { ModuleCard } from "./ModuleCard"; +import type { UiModuleBlock } from "../../types"; +import { ModuleCard } from "../frames/ModuleCard"; export function WeatherModule({ module }: { module: UiModuleBlock }) { return ; diff --git a/packages/ui/src/components/render.test.tsx b/packages/ui/src/components/render.test.tsx index 1cf40fe..010f672 100644 --- a/packages/ui/src/components/render.test.tsx +++ b/packages/ui/src/components/render.test.tsx @@ -1,14 +1,16 @@ import { renderToString } from "react-dom/server"; import { describe, expect, test } from "vitest"; -import { Button } from "./Button"; -import { DashboardFrame } from "./DashboardFrame"; -import { FooterCell } from "./FooterCell"; -import { IconButton } from "./IconButton"; -import { ServiceRow } from "./ServiceRow"; -import { StatusStrip } from "./StatusStrip"; -import { TelemetryCard } from "./TelemetryCard"; -import { TelemetryGrid } from "./TelemetryGrid"; -import { ThemeToggle } from "./ThemeToggle"; +import { + Button, + DashboardFrame, + FooterCell, + IconButton, + ServiceRow, + StatusStrip, + TelemetryCard, + TelemetryGrid, + ThemeToggle, +} from "../index"; import { dashboardPreviewFixtures } from "../fixtures"; describe("dashboard UI components", () => { diff --git a/packages/ui/src/components/LineChart.tsx b/packages/ui/src/components/telemetry/LineChart.tsx similarity index 98% rename from packages/ui/src/components/LineChart.tsx rename to packages/ui/src/components/telemetry/LineChart.tsx index 386b515..0accdf7 100644 --- a/packages/ui/src/components/LineChart.tsx +++ b/packages/ui/src/components/telemetry/LineChart.tsx @@ -1,6 +1,6 @@ import "uplot/dist/uPlot.min.css"; import { useEffect, useRef } from "react"; -import type { UiSeverity } from "../types"; +import type { UiSeverity } from "../../types"; export interface LineChartProps { values?: number[]; diff --git a/packages/ui/src/components/SignalTrace.tsx b/packages/ui/src/components/telemetry/SignalTrace.tsx similarity index 100% rename from packages/ui/src/components/SignalTrace.tsx rename to packages/ui/src/components/telemetry/SignalTrace.tsx diff --git a/packages/ui/src/components/Sparkline.tsx b/packages/ui/src/components/telemetry/Sparkline.tsx similarity index 94% rename from packages/ui/src/components/Sparkline.tsx rename to packages/ui/src/components/telemetry/Sparkline.tsx index a79f532..d03fa13 100644 --- a/packages/ui/src/components/Sparkline.tsx +++ b/packages/ui/src/components/telemetry/Sparkline.tsx @@ -1,4 +1,4 @@ -import type { UiSeverity } from "../types"; +import type { UiSeverity } from "../../types"; export interface SparklineProps { values?: number[]; diff --git a/packages/ui/src/components/TelemetryCard.tsx b/packages/ui/src/components/telemetry/TelemetryCard.tsx similarity index 91% rename from packages/ui/src/components/TelemetryCard.tsx rename to packages/ui/src/components/telemetry/TelemetryCard.tsx index 97033c9..15481ee 100644 --- a/packages/ui/src/components/TelemetryCard.tsx +++ b/packages/ui/src/components/telemetry/TelemetryCard.tsx @@ -1,7 +1,7 @@ import type { CSSProperties } from "react"; -import { clampPercent, formatMetricValue } from "../format"; -import type { UiTelemetryCard } from "../types"; -import { IconGlyph } from "./IconGlyph"; +import { clampPercent, formatMetricValue } from "../../format"; +import type { UiTelemetryCard } from "../../types"; +import { IconGlyph } from "../foundation/IconGlyph"; import { LineChart } from "./LineChart"; export interface TelemetryCardProps { diff --git a/packages/ui/src/components/TelemetryGrid.tsx b/packages/ui/src/components/telemetry/TelemetryGrid.tsx similarity index 94% rename from packages/ui/src/components/TelemetryGrid.tsx rename to packages/ui/src/components/telemetry/TelemetryGrid.tsx index 8afb9a2..9f4d497 100644 --- a/packages/ui/src/components/TelemetryGrid.tsx +++ b/packages/ui/src/components/telemetry/TelemetryGrid.tsx @@ -1,5 +1,5 @@ import { useId } from "react"; -import type { UiTelemetryCard } from "../types"; +import type { UiTelemetryCard } from "../../types"; import { TelemetryCard } from "./TelemetryCard"; export interface TelemetryGridProps { diff --git a/packages/ui/src/components/TelemetryStrip.tsx b/packages/ui/src/components/telemetry/TelemetryStrip.tsx similarity index 76% rename from packages/ui/src/components/TelemetryStrip.tsx rename to packages/ui/src/components/telemetry/TelemetryStrip.tsx index 765c1e2..35073bb 100644 --- a/packages/ui/src/components/TelemetryStrip.tsx +++ b/packages/ui/src/components/telemetry/TelemetryStrip.tsx @@ -1,4 +1,4 @@ -import type { UiTelemetryCard } from "../types"; +import type { UiTelemetryCard } from "../../types"; import { TelemetryGrid } from "./TelemetryGrid"; export function TelemetryStrip({ cards }: { cards: UiTelemetryCard[] }) { diff --git a/packages/ui/src/content-boundary.test.ts b/packages/ui/src/content-boundary.test.ts index 80b85ce..f7188a1 100644 --- a/packages/ui/src/content-boundary.test.ts +++ b/packages/ui/src/content-boundary.test.ts @@ -26,15 +26,28 @@ const forbiddenTerms = [ describe("UI package content boundary", () => { test("contains the reusable dashboard component inventory", () => { expect(existsSync(join(uiSourceRoot, "index.ts"))).toBe(true); - expect(existsSync(join(uiSourceRoot, "components/DashboardFrame.tsx"))).toBe( - true, - ); - expect(existsSync(join(uiSourceRoot, "components/ThemeToggle.tsx"))).toBe( - true, - ); + expect( + existsSync(join(uiSourceRoot, "components/frames/DashboardFrame.tsx")), + ).toBe(true); + expect( + existsSync(join(uiSourceRoot, "components/foundation/ThemeToggle.tsx")), + ).toBe(true); expect(existsSync(join(uiSourceRoot, "styles.css"))).toBe(true); }); + test("keeps UI components grouped by domain instead of a flat bucket", () => { + const domainFolders = ["foundation", "frames", "operations", "telemetry"]; + + for (const folder of domainFolders) { + expect(existsSync(join(uiSourceRoot, "components", folder))).toBe(true); + } + + const flatComponentFiles = readdirSync(join(uiSourceRoot, "components")) + .filter((entry) => entry.endsWith(".tsx") && !entry.endsWith(".test.tsx")) + .sort(); + expect(flatComponentFiles).toEqual([]); + }); + test("keeps environment-specific content out of reusable UI source", () => { const source = readUiSource(uiSourceRoot).toLowerCase(); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index a9a83df..bc506c0 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,33 +1,33 @@ -export { Badge } from "./components/Badge"; -export { Button } from "./components/Button"; -export { CornerBracketFrame } from "./components/CornerBracketFrame"; -export { DashboardHeader } from "./components/DashboardHeader"; -export { DashboardFrame } from "./components/DashboardFrame"; -export { DiagonalStripeField } from "./components/DiagonalStripeField"; -export { FooterCell } from "./components/FooterCell"; -export { FooterStatusCell } from "./components/FooterStatusCell"; -export { GridFrame } from "./components/GridFrame"; -export { IconGlyph } from "./components/IconGlyph"; -export { IconButton } from "./components/IconButton"; -export { LineChart } from "./components/LineChart"; -export { ModuleCard } from "./components/ModuleCard"; -export { Panel } from "./components/Panel"; -export { ProgressMeter } from "./components/ProgressMeter"; -export { Separator } from "./components/Separator"; -export { ServiceGroupPanel } from "./components/ServiceGroupPanel"; -export { ServicePanel } from "./components/ServicePanel"; -export { ServiceRow } from "./components/ServiceRow"; -export { ScanlineField } from "./components/ScanlineField"; -export { SignalTrace } from "./components/SignalTrace"; -export { Sparkline } from "./components/Sparkline"; -export { StatusBadge } from "./components/StatusBadge"; -export { StatusStrip } from "./components/StatusStrip"; -export { SystemState } from "./components/SystemState"; -export { TelemetryCard } from "./components/TelemetryCard"; -export { TelemetryGrid } from "./components/TelemetryGrid"; -export { TelemetryStrip } from "./components/TelemetryStrip"; -export { ThemeToggle } from "./components/ThemeToggle"; -export { WeatherModule } from "./components/WeatherModule"; +export { Badge } from "./components/foundation/Badge"; +export { Button } from "./components/foundation/Button"; +export { IconGlyph } from "./components/foundation/IconGlyph"; +export { IconButton } from "./components/foundation/IconButton"; +export { ProgressMeter } from "./components/foundation/ProgressMeter"; +export { Separator } from "./components/foundation/Separator"; +export { StatusBadge } from "./components/foundation/StatusBadge"; +export { ThemeToggle } from "./components/foundation/ThemeToggle"; +export { CornerBracketFrame } from "./components/frames/CornerBracketFrame"; +export { DashboardHeader } from "./components/frames/DashboardHeader"; +export { DashboardFrame } from "./components/frames/DashboardFrame"; +export { DiagonalStripeField } from "./components/frames/DiagonalStripeField"; +export { FooterCell } from "./components/frames/FooterCell"; +export { FooterStatusCell } from "./components/frames/FooterStatusCell"; +export { GridFrame } from "./components/frames/GridFrame"; +export { ModuleCard } from "./components/frames/ModuleCard"; +export { Panel } from "./components/frames/Panel"; +export { ScanlineField } from "./components/frames/ScanlineField"; +export { ServiceGroupPanel } from "./components/operations/ServiceGroupPanel"; +export { ServicePanel } from "./components/operations/ServicePanel"; +export { ServiceRow } from "./components/operations/ServiceRow"; +export { StatusStrip } from "./components/operations/StatusStrip"; +export { SystemState } from "./components/operations/SystemState"; +export { WeatherModule } from "./components/operations/WeatherModule"; +export { LineChart } from "./components/telemetry/LineChart"; +export { SignalTrace } from "./components/telemetry/SignalTrace"; +export { Sparkline } from "./components/telemetry/Sparkline"; +export { TelemetryCard } from "./components/telemetry/TelemetryCard"; +export { TelemetryGrid } from "./components/telemetry/TelemetryGrid"; +export { TelemetryStrip } from "./components/telemetry/TelemetryStrip"; export { dashboardPreviewFixtures } from "./fixtures"; export { getNextUiTheme, diff --git a/packages/ui/src/stories/Badge.stories.tsx b/packages/ui/src/stories/Badge.stories.tsx index d33394d..26e3ea8 100644 --- a/packages/ui/src/stories/Badge.stories.tsx +++ b/packages/ui/src/stories/Badge.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Badge } from "../components/Badge"; +import { Badge } from "../index"; const meta = { title: "UI/Badge", diff --git a/packages/ui/src/stories/Button.stories.tsx b/packages/ui/src/stories/Button.stories.tsx index c207233..084caa8 100644 --- a/packages/ui/src/stories/Button.stories.tsx +++ b/packages/ui/src/stories/Button.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Button } from "../components/Button"; +import { Button } from "../index"; const meta = { title: "UI/Button", diff --git a/packages/ui/src/stories/CornerBracketFrame.stories.tsx b/packages/ui/src/stories/CornerBracketFrame.stories.tsx index dd7efbf..d5c0110 100644 --- a/packages/ui/src/stories/CornerBracketFrame.stories.tsx +++ b/packages/ui/src/stories/CornerBracketFrame.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { CornerBracketFrame } from "../components/CornerBracketFrame"; +import { CornerBracketFrame } from "../index"; const meta = { title: "UI/CornerBracketFrame", diff --git a/packages/ui/src/stories/DashboardFrame.stories.tsx b/packages/ui/src/stories/DashboardFrame.stories.tsx index e1963af..894219e 100644 --- a/packages/ui/src/stories/DashboardFrame.stories.tsx +++ b/packages/ui/src/stories/DashboardFrame.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DashboardFrame } from "../components/DashboardFrame"; +import { DashboardFrame } from "../index"; import { fullCompositionDashboard, secondaryDashboard } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/DashboardHeader.stories.tsx b/packages/ui/src/stories/DashboardHeader.stories.tsx index 8263dd1..c337158 100644 --- a/packages/ui/src/stories/DashboardHeader.stories.tsx +++ b/packages/ui/src/stories/DashboardHeader.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DashboardHeader } from "../components/DashboardHeader"; +import { DashboardHeader } from "../index"; import { moduleBlocks } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/DashboardOnePager.stories.tsx b/packages/ui/src/stories/DashboardOnePager.stories.tsx index c44364b..3bd800f 100644 --- a/packages/ui/src/stories/DashboardOnePager.stories.tsx +++ b/packages/ui/src/stories/DashboardOnePager.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DashboardFrame } from "../components/DashboardFrame"; +import { DashboardFrame } from "../index"; import { fullCompositionDashboard } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/DiagonalStripeField.stories.tsx b/packages/ui/src/stories/DiagonalStripeField.stories.tsx index 4ca01f2..1f094ee 100644 --- a/packages/ui/src/stories/DiagonalStripeField.stories.tsx +++ b/packages/ui/src/stories/DiagonalStripeField.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { DiagonalStripeField } from "../components/DiagonalStripeField"; +import { DiagonalStripeField } from "../index"; const meta = { title: "UI/DiagonalStripeField", diff --git a/packages/ui/src/stories/FooterCell.stories.tsx b/packages/ui/src/stories/FooterCell.stories.tsx index 3ba26c7..7aa06d8 100644 --- a/packages/ui/src/stories/FooterCell.stories.tsx +++ b/packages/ui/src/stories/FooterCell.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { FooterCell } from "../components/FooterCell"; +import { FooterCell } from "../index"; import { statusItems } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/FooterStatusCell.stories.tsx b/packages/ui/src/stories/FooterStatusCell.stories.tsx index 8097ba3..bd985a1 100644 --- a/packages/ui/src/stories/FooterStatusCell.stories.tsx +++ b/packages/ui/src/stories/FooterStatusCell.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { FooterStatusCell } from "../components/FooterStatusCell"; +import { FooterStatusCell } from "../index"; import { statusItems } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/GridFrame.stories.tsx b/packages/ui/src/stories/GridFrame.stories.tsx index a43ad22..191bf6c 100644 --- a/packages/ui/src/stories/GridFrame.stories.tsx +++ b/packages/ui/src/stories/GridFrame.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { GridFrame } from "../components/GridFrame"; -import { ModuleCard } from "../components/ModuleCard"; +import { GridFrame, ModuleCard } from "../index"; import { moduleBlocks } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/IconButton.stories.tsx b/packages/ui/src/stories/IconButton.stories.tsx index 35b58b9..612bf2a 100644 --- a/packages/ui/src/stories/IconButton.stories.tsx +++ b/packages/ui/src/stories/IconButton.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { IconButton } from "../components/IconButton"; +import { IconButton } from "../index"; const meta = { title: "UI/IconButton", diff --git a/packages/ui/src/stories/IconGlyph.stories.tsx b/packages/ui/src/stories/IconGlyph.stories.tsx index 3f1715a..d832951 100644 --- a/packages/ui/src/stories/IconGlyph.stories.tsx +++ b/packages/ui/src/stories/IconGlyph.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { IconGlyph } from "../components/IconGlyph"; +import { IconGlyph } from "../index"; const meta = { title: "UI/IconGlyph", diff --git a/packages/ui/src/stories/LineChart.stories.tsx b/packages/ui/src/stories/LineChart.stories.tsx index bbf8690..ccbd502 100644 --- a/packages/ui/src/stories/LineChart.stories.tsx +++ b/packages/ui/src/stories/LineChart.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { LineChart } from "../components/LineChart"; +import { LineChart } from "../index"; const meta = { title: "UI/LineChart", diff --git a/packages/ui/src/stories/ModuleCard.stories.tsx b/packages/ui/src/stories/ModuleCard.stories.tsx index 3366a55..ea0305b 100644 --- a/packages/ui/src/stories/ModuleCard.stories.tsx +++ b/packages/ui/src/stories/ModuleCard.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ModuleCard } from "../components/ModuleCard"; +import { ModuleCard } from "../index"; import { moduleBlocks } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/Panel.stories.tsx b/packages/ui/src/stories/Panel.stories.tsx index 730c2d9..91eb61e 100644 --- a/packages/ui/src/stories/Panel.stories.tsx +++ b/packages/ui/src/stories/Panel.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Panel } from "../components/Panel"; -import { ServiceRow } from "../components/ServiceRow"; +import { Panel, ServiceRow } from "../index"; import { serviceRows } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/ProgressMeter.stories.tsx b/packages/ui/src/stories/ProgressMeter.stories.tsx index d3bfdac..eec35c2 100644 --- a/packages/ui/src/stories/ProgressMeter.stories.tsx +++ b/packages/ui/src/stories/ProgressMeter.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ProgressMeter } from "../components/ProgressMeter"; +import { ProgressMeter } from "../index"; const meta = { title: "UI/ProgressMeter", diff --git a/packages/ui/src/stories/ScanlineField.stories.tsx b/packages/ui/src/stories/ScanlineField.stories.tsx index 89e554e..fbe53f4 100644 --- a/packages/ui/src/stories/ScanlineField.stories.tsx +++ b/packages/ui/src/stories/ScanlineField.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ScanlineField } from "../components/ScanlineField"; +import { ScanlineField } from "../index"; const meta = { title: "UI/ScanlineField", diff --git a/packages/ui/src/stories/Separator.stories.tsx b/packages/ui/src/stories/Separator.stories.tsx index e261f52..986ed23 100644 --- a/packages/ui/src/stories/Separator.stories.tsx +++ b/packages/ui/src/stories/Separator.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Separator } from "../components/Separator"; +import { Separator } from "../index"; const meta = { title: "UI/Separator", diff --git a/packages/ui/src/stories/ServiceGroupPanel.stories.tsx b/packages/ui/src/stories/ServiceGroupPanel.stories.tsx index 7183771..f5ebdce 100644 --- a/packages/ui/src/stories/ServiceGroupPanel.stories.tsx +++ b/packages/ui/src/stories/ServiceGroupPanel.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ServiceGroupPanel } from "../components/ServiceGroupPanel"; +import { ServiceGroupPanel } from "../index"; import { serviceGroups } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/ServicePanel.stories.tsx b/packages/ui/src/stories/ServicePanel.stories.tsx index 750c428..d29b7ad 100644 --- a/packages/ui/src/stories/ServicePanel.stories.tsx +++ b/packages/ui/src/stories/ServicePanel.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ServicePanel } from "../components/ServicePanel"; +import { ServicePanel } from "../index"; import { serviceGroups } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/ServiceRow.stories.tsx b/packages/ui/src/stories/ServiceRow.stories.tsx index 9513d2c..56ba076 100644 --- a/packages/ui/src/stories/ServiceRow.stories.tsx +++ b/packages/ui/src/stories/ServiceRow.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ServiceRow } from "../components/ServiceRow"; +import { ServiceRow } from "../index"; import { serviceRows } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/SignalTrace.stories.tsx b/packages/ui/src/stories/SignalTrace.stories.tsx index ae8785b..4089df8 100644 --- a/packages/ui/src/stories/SignalTrace.stories.tsx +++ b/packages/ui/src/stories/SignalTrace.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { SignalTrace } from "../components/SignalTrace"; +import { SignalTrace } from "../index"; const meta = { title: "UI/SignalTrace", diff --git a/packages/ui/src/stories/Sparkline.stories.tsx b/packages/ui/src/stories/Sparkline.stories.tsx index efecfbd..af3e6e5 100644 --- a/packages/ui/src/stories/Sparkline.stories.tsx +++ b/packages/ui/src/stories/Sparkline.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Sparkline } from "../components/Sparkline"; +import { Sparkline } from "../index"; const meta = { title: "UI/Sparkline", diff --git a/packages/ui/src/stories/StatusBadge.stories.tsx b/packages/ui/src/stories/StatusBadge.stories.tsx index 4513a08..92a444d 100644 --- a/packages/ui/src/stories/StatusBadge.stories.tsx +++ b/packages/ui/src/stories/StatusBadge.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { StatusBadge } from "../components/StatusBadge"; +import { StatusBadge } from "../index"; const meta = { title: "UI/StatusBadge", diff --git a/packages/ui/src/stories/StatusStrip.stories.tsx b/packages/ui/src/stories/StatusStrip.stories.tsx index 7b808fa..2fc236d 100644 --- a/packages/ui/src/stories/StatusStrip.stories.tsx +++ b/packages/ui/src/stories/StatusStrip.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { StatusStrip } from "../components/StatusStrip"; +import { StatusStrip } from "../index"; import { mixedStatusStrip } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/SystemState.stories.tsx b/packages/ui/src/stories/SystemState.stories.tsx index 585540e..9b3f6e5 100644 --- a/packages/ui/src/stories/SystemState.stories.tsx +++ b/packages/ui/src/stories/SystemState.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { SystemState } from "../components/SystemState"; +import { SystemState } from "../index"; const meta = { title: "UI/SystemState", diff --git a/packages/ui/src/stories/TelemetryCard.stories.tsx b/packages/ui/src/stories/TelemetryCard.stories.tsx index dbdf301..3ac0e1e 100644 --- a/packages/ui/src/stories/TelemetryCard.stories.tsx +++ b/packages/ui/src/stories/TelemetryCard.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { TelemetryCard } from "../components/TelemetryCard"; +import { TelemetryCard } from "../index"; import { telemetryCards } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/TelemetryGrid.stories.tsx b/packages/ui/src/stories/TelemetryGrid.stories.tsx index 6b0166e..34493c5 100644 --- a/packages/ui/src/stories/TelemetryGrid.stories.tsx +++ b/packages/ui/src/stories/TelemetryGrid.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { TelemetryGrid } from "../components/TelemetryGrid"; +import { TelemetryGrid } from "../index"; import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/TelemetryStrip.stories.tsx b/packages/ui/src/stories/TelemetryStrip.stories.tsx index 76fc983..f7c9d13 100644 --- a/packages/ui/src/stories/TelemetryStrip.stories.tsx +++ b/packages/ui/src/stories/TelemetryStrip.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { TelemetryStrip } from "../components/TelemetryStrip"; +import { TelemetryStrip } from "../index"; import { eightTelemetryCards } from "./story-data"; const meta = { diff --git a/packages/ui/src/stories/ThemeToggle.stories.tsx b/packages/ui/src/stories/ThemeToggle.stories.tsx index 33e494a..f2edeea 100644 --- a/packages/ui/src/stories/ThemeToggle.stories.tsx +++ b/packages/ui/src/stories/ThemeToggle.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { ThemeToggle } from "../components/ThemeToggle"; +import { ThemeToggle } from "../index"; const meta = { title: "UI/ThemeToggle", diff --git a/packages/ui/src/stories/WeatherModule.stories.tsx b/packages/ui/src/stories/WeatherModule.stories.tsx index 6429de2..a376448 100644 --- a/packages/ui/src/stories/WeatherModule.stories.tsx +++ b/packages/ui/src/stories/WeatherModule.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { WeatherModule } from "../components/WeatherModule"; +import { WeatherModule } from "../index"; import { moduleBlocks } from "./story-data"; const meta = { diff --git a/packages/ui/src/storybook.test.ts b/packages/ui/src/storybook.test.ts index 4ef14ba..49a9e83 100644 --- a/packages/ui/src/storybook.test.ts +++ b/packages/ui/src/storybook.test.ts @@ -9,6 +9,56 @@ const packageRoot = existsSync(join(root, "packages/ui/package.json")) const componentsDir = join(packageRoot, "src/components"); const storiesDir = join(packageRoot, "src/stories"); +const componentDomains = { + foundation: [ + "Badge", + "Button", + "IconButton", + "IconGlyph", + "ProgressMeter", + "Separator", + "StatusBadge", + "ThemeToggle", + ], + frames: [ + "CornerBracketFrame", + "DashboardFrame", + "DashboardHeader", + "DiagonalStripeField", + "FooterCell", + "FooterStatusCell", + "GridFrame", + "ModuleCard", + "Panel", + "ScanlineField", + ], + operations: [ + "ServiceGroupPanel", + "ServicePanel", + "ServiceRow", + "StatusStrip", + "SystemState", + "WeatherModule", + ], + telemetry: [ + "LineChart", + "SignalTrace", + "Sparkline", + "TelemetryCard", + "TelemetryGrid", + "TelemetryStrip", + ], +} as const; + +const componentFiles: ReadonlyMap = new Map( + Object.entries(componentDomains).flatMap(([domain, components]) => + components.map((component) => [ + component, + join(componentsDir, domain, `${component}.tsx`), + ]), + ), +); + const requiredStoryFiles = [ "Badge.stories.tsx", "Button.stories.tsx", @@ -69,10 +119,7 @@ describe("Storybook inventory", () => { test("loads dashboard component styles through the global app stylesheet", () => { const packageStyles = readFileSync(join(packageRoot, "src/styles.css"), "utf8"); - const dashboardFrame = readFileSync( - join(componentsDir, "DashboardFrame.tsx"), - "utf8", - ); + const dashboardFrame = readFileSync(requiredComponentPath("DashboardFrame"), "utf8"); expect(packageStyles).toContain('./components/styles.css'); expect(dashboardFrame).not.toContain('./styles.css'); @@ -87,9 +134,9 @@ describe("Storybook inventory", () => { }); test("keeps component and story files paired as the UI inventory changes", () => { - const componentStoryFiles = readdirSync(componentsDir) - .filter((filename) => filename.endsWith(".tsx") && !filename.endsWith(".test.tsx")) - .map((filename) => filename.replace(".tsx", ".stories.tsx")); + const componentStoryFiles = [...componentFiles.keys()].map( + (component) => `${component}.stories.tsx`, + ); for (const filename of componentStoryFiles) { expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true); @@ -114,9 +161,7 @@ describe("Storybook inventory", () => { "ScanlineField", "SignalTrace", ]) { - expect(existsSync(join(componentsDir, `${component}.tsx`)), `${component} is missing`).toBe( - true, - ); + expect(existsSync(requiredComponentPath(component)), `${component} is missing`).toBe(true); expect( existsSync(join(storiesDir, `${component}.stories.tsx`)), `${component}.stories.tsx is missing`, @@ -131,7 +176,7 @@ describe("Storybook inventory", () => { "ScanlineField", "SignalTrace", ]) { - const source = readFileSync(join(componentsDir, `${component}.tsx`), "utf8"); + const source = readFileSync(requiredComponentPath(component), "utf8"); expect(source).toContain('aria-hidden="true"'); } @@ -139,6 +184,7 @@ describe("Storybook inventory", () => { test("does not add deferred form/navigation primitives", () => { for (const component of ["Input", "ToggleGroup", "ScrollArea"]) { + expect(componentFiles.has(component)).toBe(false); expect(existsSync(join(componentsDir, `${component}.tsx`))).toBe(false); expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false); } @@ -153,3 +199,13 @@ describe("Storybook inventory", () => { expect(legacyStories).toEqual([]); }); }); + +function requiredComponentPath(component: string): string { + const componentPath = componentFiles.get(component); + + expect(componentPath, `${component} is missing from the UI component map`).toBeTypeOf( + "string", + ); + + return componentPath as string; +} From fe8d1e56422cabff437425ecafbbee9f40c9005c Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:50:10 +0200 Subject: [PATCH 06/43] test(ui): derive story inventory from component tree --- packages/ui/src/storybook.test.ts | 201 ++++++++++++++++-------------- 1 file changed, 110 insertions(+), 91 deletions(-) diff --git a/packages/ui/src/storybook.test.ts b/packages/ui/src/storybook.test.ts index 49a9e83..83f51c7 100644 --- a/packages/ui/src/storybook.test.ts +++ b/packages/ui/src/storybook.test.ts @@ -1,5 +1,5 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { basename, join, relative } from "node:path"; import { describe, expect, test } from "vitest"; const root = process.cwd(); @@ -9,89 +9,13 @@ const packageRoot = existsSync(join(root, "packages/ui/package.json")) const componentsDir = join(packageRoot, "src/components"); const storiesDir = join(packageRoot, "src/stories"); -const componentDomains = { - foundation: [ - "Badge", - "Button", - "IconButton", - "IconGlyph", - "ProgressMeter", - "Separator", - "StatusBadge", - "ThemeToggle", - ], - frames: [ - "CornerBracketFrame", - "DashboardFrame", - "DashboardHeader", - "DiagonalStripeField", - "FooterCell", - "FooterStatusCell", - "GridFrame", - "ModuleCard", - "Panel", - "ScanlineField", - ], - operations: [ - "ServiceGroupPanel", - "ServicePanel", - "ServiceRow", - "StatusStrip", - "SystemState", - "WeatherModule", - ], - telemetry: [ - "LineChart", - "SignalTrace", - "Sparkline", - "TelemetryCard", - "TelemetryGrid", - "TelemetryStrip", - ], -} as const; - -const componentFiles: ReadonlyMap = new Map( - Object.entries(componentDomains).flatMap(([domain, components]) => - components.map((component) => [ - component, - join(componentsDir, domain, `${component}.tsx`), - ]), - ), -); - -const requiredStoryFiles = [ - "Badge.stories.tsx", - "Button.stories.tsx", - "DashboardFrame.stories.tsx", - "DashboardHeader.stories.tsx", - "DashboardOnePager.stories.tsx", - "CornerBracketFrame.stories.tsx", - "DiagonalStripeField.stories.tsx", - "FooterCell.stories.tsx", - "FooterStatusCell.stories.tsx", - "GridFrame.stories.tsx", - "IconButton.stories.tsx", - "IconGlyph.stories.tsx", - "LineChart.stories.tsx", - "ModuleCard.stories.tsx", - "Panel.stories.tsx", - "ProgressMeter.stories.tsx", - "Separator.stories.tsx", - "ServiceGroupPanel.stories.tsx", - "ServicePanel.stories.tsx", - "ServiceRow.stories.tsx", - "SignalTrace.stories.tsx", - "Sparkline.stories.tsx", - "StatusBadge.stories.tsx", - "StatusStrip.stories.tsx", - "SystemState.stories.tsx", - "ScanlineField.stories.tsx", - "TelemetryCard.stories.tsx", - "TelemetryGrid.stories.tsx", - "TelemetryStrip.stories.tsx", - "ThemeToggle.stories.tsx", - "WeatherModule.stories.tsx", +const allowedComponentDomains = [ + "foundation", + "frames", + "operations", + "telemetry", ] as const; +const compositionStoryFiles = ["DashboardOnePager.stories.tsx"] as const; const forbiddenStoryContent = [ "dimension lab", @@ -112,11 +36,23 @@ describe("Storybook inventory", () => { }); test("has a story for every reusable dashboard UI component", () => { - for (const filename of requiredStoryFiles) { - expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true); + for (const component of componentInventory()) { + expect( + existsSync(join(storiesDir, component.storyFile)), + `${component.storyFile} is missing`, + ).toBe(true); } }); + test("uses only approved component domain folders", () => { + const actualDomains = readdirSync(componentsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + expect(actualDomains).toEqual([...allowedComponentDomains].sort()); + }); + test("loads dashboard component styles through the global app stylesheet", () => { const packageStyles = readFileSync(join(packageRoot, "src/styles.css"), "utf8"); const dashboardFrame = readFileSync(requiredComponentPath("DashboardFrame"), "utf8"); @@ -134,12 +70,33 @@ describe("Storybook inventory", () => { }); test("keeps component and story files paired as the UI inventory changes", () => { - const componentStoryFiles = [...componentFiles.keys()].map( - (component) => `${component}.stories.tsx`, + const componentStoryFiles = new Set( + componentInventory().map((component) => component.storyFile), ); for (const filename of componentStoryFiles) { - expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true); + expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe( + true, + ); + } + + const unpairedStoryFiles = storyFiles().filter( + (filename) => + !componentStoryFiles.has(filename) && + !compositionStoryFiles.includes( + filename as (typeof compositionStoryFiles)[number], + ), + ); + expect(unpairedStoryFiles).toEqual([]); + }); + + test("exports every reusable component through the package barrel", () => { + const indexSource = readFileSync(join(packageRoot, "src/index.ts"), "utf8"); + + for (const component of componentInventory()) { + expect(indexSource).toContain( + `export { ${component.name} } from "${component.relativeExportPath}";`, + ); } }); @@ -183,8 +140,12 @@ describe("Storybook inventory", () => { }); test("does not add deferred form/navigation primitives", () => { + const componentNames = new Set( + componentInventory().map((component) => component.name), + ); + for (const component of ["Input", "ToggleGroup", "ScrollArea"]) { - expect(componentFiles.has(component)).toBe(false); + expect(componentNames.has(component)).toBe(false); expect(existsSync(join(componentsDir, `${component}.tsx`))).toBe(false); expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false); } @@ -201,11 +162,69 @@ describe("Storybook inventory", () => { }); function requiredComponentPath(component: string): string { - const componentPath = componentFiles.get(component); + const componentPath = componentInventory().find( + (entry) => entry.name === component, + )?.path; - expect(componentPath, `${component} is missing from the UI component map`).toBeTypeOf( + expect(componentPath, `${component} is missing from the UI component tree`).toBeTypeOf( "string", ); return componentPath as string; } + +interface ComponentInventoryItem { + name: string; + path: string; + relativeExportPath: string; + storyFile: string; +} + +function componentInventory(): ComponentInventoryItem[] { + const components = allowedComponentDomains + .flatMap((domain) => collectComponentFiles(join(componentsDir, domain))) + .sort((a, b) => a.name.localeCompare(b.name)); + const names = components.map((component) => component.name); + + expect(names).toEqual([...new Set(names)]); + + return components; +} + +function collectComponentFiles(directory: string): ComponentInventoryItem[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const entryPath = join(directory, entry.name); + + if (entry.isDirectory()) { + return collectComponentFiles(entryPath); + } + + if ( + !entry.isFile() || + !entry.name.endsWith(".tsx") || + entry.name.endsWith(".test.tsx") + ) { + return []; + } + + const name = basename(entry.name, ".tsx"); + const relativeExportPath = `./${relative(join(packageRoot, "src"), entryPath) + .replace(/\\/g, "/") + .replace(/\.tsx$/, "")}`; + + return [ + { + name, + path: entryPath, + relativeExportPath, + storyFile: `${name}.stories.tsx`, + }, + ]; + }); +} + +function storyFiles(): string[] { + return readdirSync(storiesDir) + .filter((filename) => filename.endsWith(".stories.tsx")) + .sort(); +} From 3e46a2c0cc853bb4023f2fbd0c9c8c47b3e72f5d Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:01:15 +0200 Subject: [PATCH 07/43] build(container): use turbo pruned web workspace --- .containerignore | 1 + .gitignore | 1 + README.md | 9 ++-- apps/web/Containerfile | 24 +++++---- apps/web/src/lib/workspace-boundary.test.ts | 60 ++++++++++++--------- 5 files changed, 58 insertions(+), 37 deletions(-) diff --git a/.containerignore b/.containerignore index 70b617e..b4b14d1 100644 --- a/.containerignore +++ b/.containerignore @@ -5,6 +5,7 @@ coverage data dist node_modules +out playwright-report storybook-static test-results diff --git a/.gitignore b/.gitignore index b6326bb..eed0de1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +out/ .svelte-kit/ build/ dist/ diff --git a/README.md b/README.md index 572f9bc..fee9443 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,11 @@ the checked-in `apps/web/drizzle/` directory so startup migrations can run. ### Internal Container -The checked-in `apps/web/Containerfile` builds the React client and Bun server -from the workspace root into a runtime image. For the Dimension Lab internal -host, run it behind Caddy on a loopback port and mount persistent state at -`/data`: +The checked-in `apps/web/Containerfile` runs +`turbo prune @dimensionlab/web --docker`, installs the pruned manifest set, and +builds the React client plus Bun server from the pruned workspace source. For the +Dimension Lab internal host, run it behind Caddy on a loopback port and mount +persistent state at `/data`: ```sh podman build -f apps/web/Containerfile -t localhost/dimensionlab-website:latest . diff --git a/apps/web/Containerfile b/apps/web/Containerfile index 7f12f94..9ec2baa 100644 --- a/apps/web/Containerfile +++ b/apps/web/Containerfile @@ -1,19 +1,25 @@ -FROM docker.io/oven/bun:1.3.14 AS deps +FROM docker.io/oven/bun:1.3.14 AS base WORKDIR /repo -ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH -COPY package.json bun.lock turbo.json tsconfig.base.json ./ -COPY apps/web/package.json apps/web/package.json -COPY packages/dashboard-model/package.json packages/dashboard-model/package.json -COPY packages/ui/package.json packages/ui/package.json +ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/dashboard-model/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH + +FROM base AS pruner + +COPY . . +RUN bunx turbo prune @dimensionlab/web --docker + +FROM base AS deps + +COPY --from=pruner /repo/out/json/ ./ RUN bun install --frozen-lockfile FROM deps AS build -COPY . . -RUN bun install --frozen-lockfile && bun run build +COPY --from=pruner /repo/out/full/ ./ +COPY --from=pruner /repo/tsconfig.base.json /repo/tsconfig.json ./ +RUN bun run build -FROM deps AS runtime +FROM base AS runtime WORKDIR /repo/apps/web ENV NODE_ENV=production diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 4f4200c..9c056bf 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -114,35 +114,47 @@ describe("workspace boundaries", () => { ); }); - test("stages web workspace dependency manifests before container install", () => { - const webPackage = JSON.parse( - readFileSync(join(root, "apps/web/package.json"), "utf8"), - ) as { dependencies?: Record }; - const workspacePackages = [ - "packages/ui/package.json", - "packages/dashboard-model/package.json", - ].map((manifestPath) => { - const packageJson = JSON.parse( - readFileSync(join(root, manifestPath), "utf8"), - ) as { name?: string }; - - return [packageJson.name, manifestPath] as const; - }); - const manifestsByPackageName = new Map(workspacePackages); + test("builds the internal container from a turbo-pruned web workspace", () => { const containerfile = readFileSync( join(root, "apps/web/Containerfile"), "utf8", ); + const pruneCommand = "turbo prune @dimensionlab/web --docker"; + const jsonCopy = "COPY --from=pruner /repo/out/json/ ./"; + const sourceCopy = "COPY --from=pruner /repo/out/full/ ./"; + const tsconfigCopy = + "COPY --from=pruner /repo/tsconfig.base.json /repo/tsconfig.json ./"; + const installCommand = "RUN bun install --frozen-lockfile"; - const workspaceDependencyManifests = Object.entries( - webPackage.dependencies ?? {}, - ) - .filter(([, version]) => version.startsWith("workspace:")) - .map(([packageName]) => manifestsByPackageName.get(packageName)); + expect(containerfile).toContain("AS pruner"); + expect(containerfile).toContain(pruneCommand); + expect(containerfile).toContain(jsonCopy); + expect(containerfile).toContain(sourceCopy); + expect(containerfile).toContain(tsconfigCopy); + expect(containerfile).not.toContain( + "COPY packages/dashboard-model/package.json", + ); + expect(containerfile).not.toContain("COPY packages/ui/package.json"); - expect(workspaceDependencyManifests).not.toContain(undefined); - for (const manifestPath of workspaceDependencyManifests) { - expect(containerfile).toContain(`COPY ${manifestPath} ${manifestPath}`); - } + const jsonCopyIndex = containerfile.indexOf(jsonCopy); + const installIndex = containerfile.indexOf(installCommand); + const sourceCopyIndex = containerfile.indexOf(sourceCopy); + const tsconfigCopyIndex = containerfile.indexOf(tsconfigCopy); + + expect(jsonCopyIndex).toBeGreaterThan(-1); + expect(installIndex).toBeGreaterThan(jsonCopyIndex); + expect(sourceCopyIndex).toBeGreaterThan(installIndex); + expect(tsconfigCopyIndex).toBeGreaterThan(sourceCopyIndex); + expect(containerfile.indexOf("RUN bun run build")).toBeGreaterThan( + tsconfigCopyIndex, + ); + }); + + test("keeps local turbo prune output out of git and container contexts", () => { + const gitignore = readFileSync(join(root, ".gitignore"), "utf8"); + const containerignore = readFileSync(join(root, ".containerignore"), "utf8"); + + expect(gitignore).toContain("out/"); + expect(containerignore).toContain("out"); }); }); From 74e70cf71426289e2e6614a68a56afbb3ce66159 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:12:46 +0200 Subject: [PATCH 08/43] fix(container): exclude generated files from pruned build --- .containerignore | 9 ++++++++ apps/web/package.json | 2 +- apps/web/src/lib/workspace-boundary.test.ts | 25 +++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.containerignore b/.containerignore index b4b14d1..1812091 100644 --- a/.containerignore +++ b/.containerignore @@ -11,3 +11,12 @@ storybook-static test-results .env .env.* +apps/*/.turbo +apps/*/build +apps/*/data +apps/*/dist +apps/*/playwright-report +apps/*/test-results +packages/*/.turbo +packages/*/dist +packages/*/storybook-static diff --git a/apps/web/package.json b/apps/web/package.json index 2e9fef6..790663c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "bun src/server/dev.ts", - "build": "vite build && bun build src/server/index.ts --target bun --outdir build", + "build": "rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build", "preview": "HOST=0.0.0.0 PORT=4173 bun build/index.js", "check": "tsc --noEmit", "test": "vitest run", diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 9c056bf..9cde8e8 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -151,10 +151,35 @@ describe("workspace boundaries", () => { }); test("keeps local turbo prune output out of git and container contexts", () => { + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { scripts?: Record }; const gitignore = readFileSync(join(root, ".gitignore"), "utf8"); const containerignore = readFileSync(join(root, ".containerignore"), "utf8"); + const containerIgnoreRules = new Set( + containerignore + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean), + ); expect(gitignore).toContain("out/"); expect(containerignore).toContain("out"); + expect([...containerIgnoreRules]).toEqual( + expect.arrayContaining([ + "apps/*/.turbo", + "apps/*/build", + "apps/*/data", + "apps/*/dist", + "apps/*/playwright-report", + "apps/*/test-results", + "packages/*/.turbo", + "packages/*/dist", + "packages/*/storybook-static", + ]), + ); + expect(webPackage.scripts?.build).toBe( + "rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build", + ); }); }); From a77826eab9354717c9bc39377adba47c8a8ff2e4 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:20:18 +0200 Subject: [PATCH 09/43] refactor(turbo): consume workspace package exports --- apps/web/src/lib/workspace-boundary.test.ts | 27 +++++++++++++++++++++ apps/web/tsconfig.json | 6 ----- apps/web/vite.config.ts | 12 --------- turbo.json | 1 + 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 9cde8e8..3bb7f85 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -114,6 +114,33 @@ describe("workspace boundaries", () => { ); }); + test("consumes workspace packages through package exports instead of source aliases", () => { + const webTsconfig = JSON.parse( + readFileSync(join(root, "apps/web/tsconfig.json"), "utf8"), + ) as { compilerOptions?: { paths?: Record } }; + const viteConfig = readFileSync(join(root, "apps/web/vite.config.ts"), "utf8"); + const turboConfig = JSON.parse( + readFileSync(join(root, "turbo.json"), "utf8"), + ) as { tasks?: Record }; + + expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty( + "@dimensionlab/dashboard-model", + ); + expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty( + "@dimensionlab/dashboard-model/fixtures", + ); + expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty("@dimensionlab/ui"); + expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty( + "@dimensionlab/ui/styles.css", + ); + expect(webTsconfig.compilerOptions?.paths).toEqual({ + "$lib/*": ["src/lib/*"], + }); + expect(viteConfig).not.toContain("../../packages/dashboard-model/src"); + expect(viteConfig).not.toContain("../../packages/ui/src"); + expect(turboConfig.tasks?.dev?.dependsOn).toEqual(["^build"]); + }); + test("builds the internal container from a turbo-pruned web workspace", () => { const containerfile = readFileSync( join(root, "apps/web/Containerfile"), diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index f9f784d..120931b 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -3,12 +3,6 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@dimensionlab/dashboard-model": ["../../packages/dashboard-model/src/index.ts"], - "@dimensionlab/dashboard-model/fixtures": [ - "../../packages/dashboard-model/src/fixtures/index.ts" - ], - "@dimensionlab/ui": ["../../packages/ui/src/index.ts"], - "@dimensionlab/ui/styles.css": ["../../packages/ui/src/styles.css"], "$lib/*": ["src/lib/*"] }, "types": ["node", "bun-types", "react", "react-dom", "vite/client"] diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 687c180..ceff746 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -24,18 +24,6 @@ export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { - "@dimensionlab/ui/styles.css": fileURLToPath( - new URL("../../packages/ui/src/styles.css", import.meta.url), - ), - "@dimensionlab/ui": fileURLToPath( - new URL("../../packages/ui/src/index.ts", import.meta.url), - ), - "@dimensionlab/dashboard-model/fixtures": fileURLToPath( - new URL("../../packages/dashboard-model/src/fixtures/index.ts", import.meta.url), - ), - "@dimensionlab/dashboard-model": fileURLToPath( - new URL("../../packages/dashboard-model/src/index.ts", import.meta.url), - ), $lib: fileURLToPath(new URL("./src/lib", import.meta.url)), }, }, diff --git a/turbo.json b/turbo.json index 446d6fe..f2d3137 100644 --- a/turbo.json +++ b/turbo.json @@ -60,6 +60,7 @@ "env": ["DATABASE_URL"] }, "dev": { + "dependsOn": ["^build"], "cache": false, "persistent": true, "env": [ From 2d779165cdbd4bf999e053ef1e60a37dc9b78706 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:26:29 +0200 Subject: [PATCH 10/43] fix(turbo): keep package exports fresh in dev --- apps/web/src/lib/workspace-boundary.test.ts | 44 +++++++++++++++++++++ apps/web/src/server/dev.test.ts | 8 ++++ apps/web/src/server/dev.ts | 6 ++- packages/dashboard-model/package.json | 2 + packages/ui/package.json | 11 +++++- 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 3bb7f85..2f1bfb1 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -118,6 +118,12 @@ describe("workspace boundaries", () => { const webTsconfig = JSON.parse( readFileSync(join(root, "apps/web/tsconfig.json"), "utf8"), ) as { compilerOptions?: { paths?: Record } }; + const modelPackage = JSON.parse( + readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"), + ) as { exports?: Record }; + const uiPackage = JSON.parse( + readFileSync(join(root, "packages/ui/package.json"), "utf8"), + ) as { exports?: Record }; const viteConfig = readFileSync(join(root, "apps/web/vite.config.ts"), "utf8"); const turboConfig = JSON.parse( readFileSync(join(root, "turbo.json"), "utf8"), @@ -139,6 +145,29 @@ describe("workspace boundaries", () => { expect(viteConfig).not.toContain("../../packages/dashboard-model/src"); expect(viteConfig).not.toContain("../../packages/ui/src"); expect(turboConfig.tasks?.dev?.dependsOn).toEqual(["^build"]); + expectPackageExport(modelPackage.exports?.["."], { + types: "./dist/index.d.ts", + development: "./src/index.ts", + default: "./dist/index.js", + }); + expectPackageExport(modelPackage.exports?.["./fixtures"], { + types: "./dist/fixtures/index.d.ts", + development: "./src/fixtures/index.ts", + default: "./dist/fixtures/index.js", + }); + expectPackageExport(uiPackage.exports?.["."], { + types: "./dist/index.d.ts", + development: "./src/index.ts", + default: "./dist/index.js", + }); + expectPackageExport(uiPackage.exports?.["./styles.css"], { + development: "./src/styles.css", + default: "./dist/styles.css", + }); + expectPackageExport(uiPackage.exports?.["./tokens.css"], { + development: "./src/tokens.css", + default: "./dist/tokens.css", + }); }); test("builds the internal container from a turbo-pruned web workspace", () => { @@ -210,3 +239,18 @@ describe("workspace boundaries", () => { ); }); }); + +type WorkspacePackageExport = + | string + | { + types?: string; + development?: string; + default?: string; + }; + +function expectPackageExport( + actual: WorkspacePackageExport | undefined, + expected: Exclude, +): void { + expect(actual).toMatchObject(expected); +} diff --git a/apps/web/src/server/dev.test.ts b/apps/web/src/server/dev.test.ts index f43f953..60031db 100644 --- a/apps/web/src/server/dev.test.ts +++ b/apps/web/src/server/dev.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { createDevServerConfig } from "../../vite.config"; +import { apiServerArgs } from "./dev"; describe("local development runtime", () => { test("starts the Bun API server together with the Vite dev server", () => { @@ -22,4 +23,11 @@ describe("local development runtime", () => { changeOrigin: true, }); }); + + test("uses development package export conditions for the Bun API server", () => { + expect(apiServerArgs).toEqual([ + "--conditions=development", + "src/server/index.ts", + ]); + }); }); diff --git a/apps/web/src/server/dev.ts b/apps/web/src/server/dev.ts index c515240..fa4dc2f 100644 --- a/apps/web/src/server/dev.ts +++ b/apps/web/src/server/dev.ts @@ -3,6 +3,10 @@ const webPort = process.env.PORT || "5173"; const apiHost = process.env.DASHBOARD_DEV_API_HOST || "127.0.0.1"; const apiPort = process.env.DASHBOARD_DEV_API_PORT || "5174"; const apiTarget = `http://${apiHost}:${apiPort}`; +export const apiServerArgs = [ + "--conditions=development", + "src/server/index.ts", +] as const; if (import.meta.main) { runDevServers(); @@ -51,7 +55,7 @@ export function runDevServers(): void { process.on("SIGINT", () => shutdown(0)); process.on("SIGTERM", () => shutdown(0)); - spawn("api server", [process.execPath, "src/server/index.ts"], { + spawn("api server", [process.execPath, ...apiServerArgs], { HOST: apiHost, PORT: apiPort, }); diff --git a/packages/dashboard-model/package.json b/packages/dashboard-model/package.json index 978522e..ae9a49b 100644 --- a/packages/dashboard-model/package.json +++ b/packages/dashboard-model/package.json @@ -6,10 +6,12 @@ "exports": { ".": { "types": "./dist/index.d.ts", + "development": "./src/index.ts", "default": "./dist/index.js" }, "./fixtures": { "types": "./dist/fixtures/index.d.ts", + "development": "./src/fixtures/index.ts", "default": "./dist/fixtures/index.js" } }, diff --git a/packages/ui/package.json b/packages/ui/package.json index cb7febf..897043c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,10 +7,17 @@ "exports": { ".": { "types": "./dist/index.d.ts", + "development": "./src/index.ts", "default": "./dist/index.js" }, - "./styles.css": "./dist/styles.css", - "./tokens.css": "./dist/tokens.css" + "./styles.css": { + "development": "./src/styles.css", + "default": "./dist/styles.css" + }, + "./tokens.css": { + "development": "./src/tokens.css", + "default": "./dist/tokens.css" + } }, "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/components && cp src/styles.css dist/styles.css && cp src/tokens.css dist/tokens.css && cp src/components/styles.css dist/components/styles.css", From ef43aac6dfab3367833e8665cf045bd879c622c0 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:31:35 +0200 Subject: [PATCH 11/43] feat(ui): expose grouped component entrypoints --- packages/ui/package.json | 20 ++++++++ .../ui/src/components/foundation/index.ts | 8 +++ packages/ui/src/components/frames/index.ts | 10 ++++ .../ui/src/components/operations/index.ts | 6 +++ packages/ui/src/components/telemetry/index.ts | 6 +++ packages/ui/src/index.ts | 4 ++ packages/ui/src/storybook.test.ts | 51 +++++++++++++++++++ 7 files changed, 105 insertions(+) create mode 100644 packages/ui/src/components/foundation/index.ts create mode 100644 packages/ui/src/components/frames/index.ts create mode 100644 packages/ui/src/components/operations/index.ts create mode 100644 packages/ui/src/components/telemetry/index.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 897043c..a30db52 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -10,6 +10,26 @@ "development": "./src/index.ts", "default": "./dist/index.js" }, + "./foundation": { + "types": "./dist/components/foundation/index.d.ts", + "development": "./src/components/foundation/index.ts", + "default": "./dist/components/foundation/index.js" + }, + "./frames": { + "types": "./dist/components/frames/index.d.ts", + "development": "./src/components/frames/index.ts", + "default": "./dist/components/frames/index.js" + }, + "./operations": { + "types": "./dist/components/operations/index.d.ts", + "development": "./src/components/operations/index.ts", + "default": "./dist/components/operations/index.js" + }, + "./telemetry": { + "types": "./dist/components/telemetry/index.d.ts", + "development": "./src/components/telemetry/index.ts", + "default": "./dist/components/telemetry/index.js" + }, "./styles.css": { "development": "./src/styles.css", "default": "./dist/styles.css" diff --git a/packages/ui/src/components/foundation/index.ts b/packages/ui/src/components/foundation/index.ts new file mode 100644 index 0000000..36ed868 --- /dev/null +++ b/packages/ui/src/components/foundation/index.ts @@ -0,0 +1,8 @@ +export { Badge } from "./Badge"; +export { Button } from "./Button"; +export { IconButton } from "./IconButton"; +export { IconGlyph } from "./IconGlyph"; +export { ProgressMeter } from "./ProgressMeter"; +export { Separator } from "./Separator"; +export { StatusBadge } from "./StatusBadge"; +export { ThemeToggle } from "./ThemeToggle"; diff --git a/packages/ui/src/components/frames/index.ts b/packages/ui/src/components/frames/index.ts new file mode 100644 index 0000000..66a48c9 --- /dev/null +++ b/packages/ui/src/components/frames/index.ts @@ -0,0 +1,10 @@ +export { CornerBracketFrame } from "./CornerBracketFrame"; +export { DashboardFrame } from "./DashboardFrame"; +export { DashboardHeader } from "./DashboardHeader"; +export { DiagonalStripeField } from "./DiagonalStripeField"; +export { FooterCell } from "./FooterCell"; +export { FooterStatusCell } from "./FooterStatusCell"; +export { GridFrame } from "./GridFrame"; +export { ModuleCard } from "./ModuleCard"; +export { Panel } from "./Panel"; +export { ScanlineField } from "./ScanlineField"; diff --git a/packages/ui/src/components/operations/index.ts b/packages/ui/src/components/operations/index.ts new file mode 100644 index 0000000..27ab623 --- /dev/null +++ b/packages/ui/src/components/operations/index.ts @@ -0,0 +1,6 @@ +export { ServiceGroupPanel } from "./ServiceGroupPanel"; +export { ServicePanel } from "./ServicePanel"; +export { ServiceRow } from "./ServiceRow"; +export { StatusStrip } from "./StatusStrip"; +export { SystemState } from "./SystemState"; +export { WeatherModule } from "./WeatherModule"; diff --git a/packages/ui/src/components/telemetry/index.ts b/packages/ui/src/components/telemetry/index.ts new file mode 100644 index 0000000..4ba32fa --- /dev/null +++ b/packages/ui/src/components/telemetry/index.ts @@ -0,0 +1,6 @@ +export { LineChart } from "./LineChart"; +export { SignalTrace } from "./SignalTrace"; +export { Sparkline } from "./Sparkline"; +export { TelemetryCard } from "./TelemetryCard"; +export { TelemetryGrid } from "./TelemetryGrid"; +export { TelemetryStrip } from "./TelemetryStrip"; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index bc506c0..e1fe97e 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,3 +1,7 @@ +export * from "./components/foundation"; +export * from "./components/frames"; +export * from "./components/operations"; +export * from "./components/telemetry"; export { Badge } from "./components/foundation/Badge"; export { Button } from "./components/foundation/Button"; export { IconGlyph } from "./components/foundation/IconGlyph"; diff --git a/packages/ui/src/storybook.test.ts b/packages/ui/src/storybook.test.ts index 83f51c7..050170b 100644 --- a/packages/ui/src/storybook.test.ts +++ b/packages/ui/src/storybook.test.ts @@ -100,6 +100,43 @@ describe("Storybook inventory", () => { } }); + test("exposes grouped package entrypoints for each component domain", () => { + const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as { + exports?: Record; + }; + const rootIndexSource = readFileSync(join(packageRoot, "src/index.ts"), "utf8"); + const componentsByDomain = new Map<(typeof allowedComponentDomains)[number], string[]>(); + + for (const domain of allowedComponentDomains) { + componentsByDomain.set(domain, []); + } + + for (const component of componentInventory()) { + componentsByDomain.get(component.domain)?.push(component.name); + } + + for (const domain of allowedComponentDomains) { + const domainIndexPath = join(componentsDir, domain, "index.ts"); + const domainIndexSource = existsSync(domainIndexPath) + ? readFileSync(domainIndexPath, "utf8") + : ""; + + expect(existsSync(domainIndexPath), `${domain} index is missing`).toBe(true); + expect(rootIndexSource).toContain(`export * from "./components/${domain}";`); + expect(packageJson.exports?.[`./${domain}`]).toMatchObject({ + types: `./dist/components/${domain}/index.d.ts`, + development: `./src/components/${domain}/index.ts`, + default: `./dist/components/${domain}/index.js`, + }); + + for (const componentName of componentsByDomain.get(domain) ?? []) { + expect(domainIndexSource).toContain( + `export { ${componentName} } from "./${componentName}";`, + ); + } + } + }); + test("keeps Storybook fixtures generic and content-free", () => { const storyText = readdirSync(storiesDir) .filter((filename) => filename.endsWith(".tsx") || filename.endsWith(".ts")) @@ -174,12 +211,21 @@ function requiredComponentPath(component: string): string { } interface ComponentInventoryItem { + domain: (typeof allowedComponentDomains)[number]; name: string; path: string; relativeExportPath: string; storyFile: string; } +type WorkspacePackageExport = + | string + | { + types?: string; + development?: string; + default?: string; + }; + function componentInventory(): ComponentInventoryItem[] { const components = allowedComponentDomains .flatMap((domain) => collectComponentFiles(join(componentsDir, domain))) @@ -208,12 +254,17 @@ function collectComponentFiles(directory: string): ComponentInventoryItem[] { } const name = basename(entry.name, ".tsx"); + const relativeComponentPath = relative(componentsDir, entryPath).replace(/\\/g, "/"); + const domain = relativeComponentPath.split( + "/", + )[0] as (typeof allowedComponentDomains)[number]; const relativeExportPath = `./${relative(join(packageRoot, "src"), entryPath) .replace(/\\/g, "/") .replace(/\.tsx$/, "")}`; return [ { + domain, name, path: entryPath, relativeExportPath, From 2535dccff2650d4c31ecb33579b4d0f5f95f2861 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:37:15 +0200 Subject: [PATCH 12/43] feat(ui): export component prop types --- .../ui/src/components/foundation/index.ts | 16 +++++++------- packages/ui/src/components/frames/index.ts | 20 ++++++++--------- .../ui/src/components/operations/index.ts | 12 +++++----- packages/ui/src/components/telemetry/index.ts | 12 +++++----- packages/ui/src/storybook.test.ts | 22 ++++++++++++++++++- 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/components/foundation/index.ts b/packages/ui/src/components/foundation/index.ts index 36ed868..b82cd47 100644 --- a/packages/ui/src/components/foundation/index.ts +++ b/packages/ui/src/components/foundation/index.ts @@ -1,8 +1,8 @@ -export { Badge } from "./Badge"; -export { Button } from "./Button"; -export { IconButton } from "./IconButton"; -export { IconGlyph } from "./IconGlyph"; -export { ProgressMeter } from "./ProgressMeter"; -export { Separator } from "./Separator"; -export { StatusBadge } from "./StatusBadge"; -export { ThemeToggle } from "./ThemeToggle"; +export * from "./Badge"; +export * from "./Button"; +export * from "./IconButton"; +export * from "./IconGlyph"; +export * from "./ProgressMeter"; +export * from "./Separator"; +export * from "./StatusBadge"; +export * from "./ThemeToggle"; diff --git a/packages/ui/src/components/frames/index.ts b/packages/ui/src/components/frames/index.ts index 66a48c9..80b093d 100644 --- a/packages/ui/src/components/frames/index.ts +++ b/packages/ui/src/components/frames/index.ts @@ -1,10 +1,10 @@ -export { CornerBracketFrame } from "./CornerBracketFrame"; -export { DashboardFrame } from "./DashboardFrame"; -export { DashboardHeader } from "./DashboardHeader"; -export { DiagonalStripeField } from "./DiagonalStripeField"; -export { FooterCell } from "./FooterCell"; -export { FooterStatusCell } from "./FooterStatusCell"; -export { GridFrame } from "./GridFrame"; -export { ModuleCard } from "./ModuleCard"; -export { Panel } from "./Panel"; -export { ScanlineField } from "./ScanlineField"; +export * from "./CornerBracketFrame"; +export * from "./DashboardFrame"; +export * from "./DashboardHeader"; +export * from "./DiagonalStripeField"; +export * from "./FooterCell"; +export * from "./FooterStatusCell"; +export * from "./GridFrame"; +export * from "./ModuleCard"; +export * from "./Panel"; +export * from "./ScanlineField"; diff --git a/packages/ui/src/components/operations/index.ts b/packages/ui/src/components/operations/index.ts index 27ab623..127a1e0 100644 --- a/packages/ui/src/components/operations/index.ts +++ b/packages/ui/src/components/operations/index.ts @@ -1,6 +1,6 @@ -export { ServiceGroupPanel } from "./ServiceGroupPanel"; -export { ServicePanel } from "./ServicePanel"; -export { ServiceRow } from "./ServiceRow"; -export { StatusStrip } from "./StatusStrip"; -export { SystemState } from "./SystemState"; -export { WeatherModule } from "./WeatherModule"; +export * from "./ServiceGroupPanel"; +export * from "./ServicePanel"; +export * from "./ServiceRow"; +export * from "./StatusStrip"; +export * from "./SystemState"; +export * from "./WeatherModule"; diff --git a/packages/ui/src/components/telemetry/index.ts b/packages/ui/src/components/telemetry/index.ts index 4ba32fa..a77693e 100644 --- a/packages/ui/src/components/telemetry/index.ts +++ b/packages/ui/src/components/telemetry/index.ts @@ -1,6 +1,6 @@ -export { LineChart } from "./LineChart"; -export { SignalTrace } from "./SignalTrace"; -export { Sparkline } from "./Sparkline"; -export { TelemetryCard } from "./TelemetryCard"; -export { TelemetryGrid } from "./TelemetryGrid"; -export { TelemetryStrip } from "./TelemetryStrip"; +export * from "./LineChart"; +export * from "./SignalTrace"; +export * from "./Sparkline"; +export * from "./TelemetryCard"; +export * from "./TelemetryGrid"; +export * from "./TelemetryStrip"; diff --git a/packages/ui/src/storybook.test.ts b/packages/ui/src/storybook.test.ts index 050170b..9b73c69 100644 --- a/packages/ui/src/storybook.test.ts +++ b/packages/ui/src/storybook.test.ts @@ -131,12 +131,32 @@ describe("Storybook inventory", () => { for (const componentName of componentsByDomain.get(domain) ?? []) { expect(domainIndexSource).toContain( - `export { ${componentName} } from "./${componentName}";`, + `export * from "./${componentName}";`, ); } } }); + test("exports component prop interfaces through grouped package entrypoints", () => { + for (const component of componentInventory()) { + const source = readFileSync(component.path, "utf8"); + const propTypeName = `${component.name}Props`; + + if (!source.match(new RegExp(`export\\\\s+(interface|type)\\\\s+${propTypeName}\\\\b`))) { + continue; + } + + const domainIndexSource = readFileSync( + join(componentsDir, component.domain, "index.ts"), + "utf8", + ); + + expect(domainIndexSource).toContain( + `export * from "./${component.name}";`, + ); + } + }); + test("keeps Storybook fixtures generic and content-free", () => { const storyText = readdirSync(storiesDir) .filter((filename) => filename.endsWith(".tsx") || filename.endsWith(".ts")) From 5dccd70e0b3527fd827ee9b14f6cd9fd4a854f80 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:40:52 +0200 Subject: [PATCH 13/43] test(ui): assert public prop exports are detected --- packages/ui/src/storybook.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/storybook.test.ts b/packages/ui/src/storybook.test.ts index 9b73c69..9a18ecd 100644 --- a/packages/ui/src/storybook.test.ts +++ b/packages/ui/src/storybook.test.ts @@ -138,14 +138,20 @@ describe("Storybook inventory", () => { }); test("exports component prop interfaces through grouped package entrypoints", () => { + let propInterfaceCount = 0; + for (const component of componentInventory()) { const source = readFileSync(component.path, "utf8"); const propTypeName = `${component.name}Props`; + const propExportPattern = new RegExp( + `export\\s+(interface|type)\\s+${propTypeName}\\b`, + ); - if (!source.match(new RegExp(`export\\\\s+(interface|type)\\\\s+${propTypeName}\\\\b`))) { + if (!propExportPattern.test(source)) { continue; } + propInterfaceCount += 1; const domainIndexSource = readFileSync( join(componentsDir, component.domain, "index.ts"), "utf8", @@ -155,6 +161,8 @@ describe("Storybook inventory", () => { `export * from "./${component.name}";`, ); } + + expect(propInterfaceCount).toBeGreaterThan(0); }); test("keeps Storybook fixtures generic and content-free", () => { From 24e7e8eda0445a2fd45cfe78548521bfed1caf39 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:48:54 +0200 Subject: [PATCH 14/43] fix(turbo): hash local env inputs --- apps/web/src/lib/workspace-boundary.test.ts | 15 +++++++++++++++ turbo.json | 2 ++ 2 files changed, 17 insertions(+) diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 2f1bfb1..38a3f1b 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -74,6 +74,21 @@ describe("workspace boundaries", () => { ); }); + test("accounts for local env files in cacheable Vite and Storybook task hashes", () => { + const turboConfig = JSON.parse( + readFileSync(join(root, "turbo.json"), "utf8"), + ) as { + tasks?: Record; + }; + + for (const taskName of ["build", "build-storybook"]) { + expect(turboConfig.tasks?.[taskName]?.inputs).toEqual([ + "$TURBO_DEFAULT$", + ".env*", + ]); + } + }); + test("keeps the website app and reusable UI library as separate packages", () => { const webPackage = JSON.parse( readFileSync(join(root, "apps/web/package.json"), "utf8"), diff --git a/turbo.json b/turbo.json index f2d3137..e1b2e7c 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,7 @@ "tasks": { "build": { "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**", "build/**"], "env": ["NODE_ENV", "VITE_*"] }, @@ -29,6 +30,7 @@ }, "build-storybook": { "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["storybook-static/**"], "env": ["NODE_ENV", "STORYBOOK_*", "VITE_*"] }, From 7a3a5b8df731f7a78273705cdc8ecde0a1c37313 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 12:11:45 +0200 Subject: [PATCH 15/43] refactor: move ui package to submodule --- .gitmodules | 4 + packages/ui | 1 + packages/ui/.storybook/main.ts | 15 - packages/ui/.storybook/preview.ts | 51 - packages/ui/components.json | 25 - packages/ui/package.json | 79 -- .../ui/src/components/foundation/Badge.tsx | 11 - .../ui/src/components/foundation/Button.tsx | 39 - .../src/components/foundation/IconButton.tsx | 33 - .../src/components/foundation/IconGlyph.tsx | 21 - .../components/foundation/ProgressMeter.tsx | 34 - .../src/components/foundation/Separator.tsx | 19 - .../src/components/foundation/StatusBadge.tsx | 14 - .../src/components/foundation/ThemeToggle.tsx | 36 - .../ui/src/components/foundation/index.ts | 8 - .../components/frames/CornerBracketFrame.tsx | 26 - .../src/components/frames/DashboardFrame.tsx | 59 -- .../src/components/frames/DashboardHeader.tsx | 30 - .../components/frames/DiagonalStripeField.tsx | 24 - .../ui/src/components/frames/FooterCell.tsx | 42 - .../components/frames/FooterStatusCell.tsx | 6 - .../ui/src/components/frames/GridFrame.tsx | 13 - .../ui/src/components/frames/ModuleCard.tsx | 32 - packages/ui/src/components/frames/Panel.tsx | 19 - .../src/components/frames/ScanlineField.tsx | 21 - packages/ui/src/components/frames/index.ts | 10 - .../operations/ServiceGroupPanel.tsx | 6 - .../components/operations/ServicePanel.tsx | 27 - .../src/components/operations/ServiceRow.tsx | 56 - .../src/components/operations/StatusStrip.tsx | 23 - .../src/components/operations/SystemState.tsx | 26 - .../components/operations/WeatherModule.tsx | 6 - .../ui/src/components/operations/index.ts | 6 - packages/ui/src/components/render.test.tsx | 204 ---- packages/ui/src/components/styles.css | 957 ------------------ .../ui/src/components/telemetry/LineChart.tsx | 114 --- .../src/components/telemetry/SignalTrace.tsx | 26 - .../ui/src/components/telemetry/Sparkline.tsx | 37 - .../components/telemetry/TelemetryCard.tsx | 59 -- .../components/telemetry/TelemetryGrid.tsx | 29 - .../components/telemetry/TelemetryStrip.tsx | 6 - packages/ui/src/components/telemetry/index.ts | 6 - packages/ui/src/content-boundary.test.ts | 87 -- packages/ui/src/css.d.ts | 1 - packages/ui/src/fixtures.ts | 132 --- packages/ui/src/format.ts | 43 - packages/ui/src/index.ts | 54 - packages/ui/src/primitives/alert.tsx | 76 -- packages/ui/src/primitives/badge.tsx | 49 - packages/ui/src/primitives/button.tsx | 67 -- packages/ui/src/primitives/card.tsx | 103 -- packages/ui/src/primitives/progress.tsx | 29 - packages/ui/src/primitives/separator.tsx | 28 - packages/ui/src/primitives/skeleton.tsx | 13 - packages/ui/src/stories/Badge.stories.tsx | 16 - packages/ui/src/stories/Button.stories.tsx | 21 - .../stories/CornerBracketFrame.stories.tsx | 16 - .../ui/src/stories/DashboardFrame.stories.tsx | 21 - .../src/stories/DashboardHeader.stories.tsx | 19 - .../src/stories/DashboardOnePager.stories.tsx | 16 - .../stories/DiagonalStripeField.stories.tsx | 16 - .../ui/src/stories/FooterCell.stories.tsx | 21 - .../src/stories/FooterStatusCell.stories.tsx | 16 - packages/ui/src/stories/GridFrame.stories.tsx | 24 - .../ui/src/stories/IconButton.stories.tsx | 16 - packages/ui/src/stories/IconGlyph.stories.tsx | 17 - packages/ui/src/stories/LineChart.stories.tsx | 17 - .../ui/src/stories/ModuleCard.stories.tsx | 21 - packages/ui/src/stories/Panel.stories.tsx | 22 - .../ui/src/stories/ProgressMeter.stories.tsx | 17 - .../ui/src/stories/ScanlineField.stories.tsx | 16 - packages/ui/src/stories/Separator.stories.tsx | 15 - .../src/stories/ServiceGroupPanel.stories.tsx | 16 - .../ui/src/stories/ServicePanel.stories.tsx | 16 - .../ui/src/stories/ServiceRow.stories.tsx | 21 - .../ui/src/stories/SignalTrace.stories.tsx | 16 - packages/ui/src/stories/Sparkline.stories.tsx | 16 - .../ui/src/stories/StatusBadge.stories.tsx | 16 - .../ui/src/stories/StatusStrip.stories.tsx | 17 - .../ui/src/stories/SystemState.stories.tsx | 18 - .../ui/src/stories/TelemetryCard.stories.tsx | 21 - .../ui/src/stories/TelemetryGrid.stories.tsx | 21 - .../ui/src/stories/TelemetryStrip.stories.tsx | 16 - .../ui/src/stories/ThemeToggle.stories.tsx | 26 - .../ui/src/stories/WeatherModule.stories.tsx | 16 - packages/ui/src/stories/story-data.ts | 258 ----- packages/ui/src/storybook.test.ts | 309 ------ packages/ui/src/styles.css | 4 - packages/ui/src/theme.test.ts | 56 - packages/ui/src/theme.ts | 68 -- packages/ui/src/tokens.css | 160 --- packages/ui/src/types.ts | 88 -- packages/ui/src/utils.ts | 6 - packages/ui/tsconfig.build.json | 19 - packages/ui/tsconfig.json | 8 - 95 files changed, 5 insertions(+), 4476 deletions(-) create mode 100644 .gitmodules create mode 160000 packages/ui delete mode 100644 packages/ui/.storybook/main.ts delete mode 100644 packages/ui/.storybook/preview.ts delete mode 100644 packages/ui/components.json delete mode 100644 packages/ui/package.json delete mode 100644 packages/ui/src/components/foundation/Badge.tsx delete mode 100644 packages/ui/src/components/foundation/Button.tsx delete mode 100644 packages/ui/src/components/foundation/IconButton.tsx delete mode 100644 packages/ui/src/components/foundation/IconGlyph.tsx delete mode 100644 packages/ui/src/components/foundation/ProgressMeter.tsx delete mode 100644 packages/ui/src/components/foundation/Separator.tsx delete mode 100644 packages/ui/src/components/foundation/StatusBadge.tsx delete mode 100644 packages/ui/src/components/foundation/ThemeToggle.tsx delete mode 100644 packages/ui/src/components/foundation/index.ts delete mode 100644 packages/ui/src/components/frames/CornerBracketFrame.tsx delete mode 100644 packages/ui/src/components/frames/DashboardFrame.tsx delete mode 100644 packages/ui/src/components/frames/DashboardHeader.tsx delete mode 100644 packages/ui/src/components/frames/DiagonalStripeField.tsx delete mode 100644 packages/ui/src/components/frames/FooterCell.tsx delete mode 100644 packages/ui/src/components/frames/FooterStatusCell.tsx delete mode 100644 packages/ui/src/components/frames/GridFrame.tsx delete mode 100644 packages/ui/src/components/frames/ModuleCard.tsx delete mode 100644 packages/ui/src/components/frames/Panel.tsx delete mode 100644 packages/ui/src/components/frames/ScanlineField.tsx delete mode 100644 packages/ui/src/components/frames/index.ts delete mode 100644 packages/ui/src/components/operations/ServiceGroupPanel.tsx delete mode 100644 packages/ui/src/components/operations/ServicePanel.tsx delete mode 100644 packages/ui/src/components/operations/ServiceRow.tsx delete mode 100644 packages/ui/src/components/operations/StatusStrip.tsx delete mode 100644 packages/ui/src/components/operations/SystemState.tsx delete mode 100644 packages/ui/src/components/operations/WeatherModule.tsx delete mode 100644 packages/ui/src/components/operations/index.ts delete mode 100644 packages/ui/src/components/render.test.tsx delete mode 100644 packages/ui/src/components/styles.css delete mode 100644 packages/ui/src/components/telemetry/LineChart.tsx delete mode 100644 packages/ui/src/components/telemetry/SignalTrace.tsx delete mode 100644 packages/ui/src/components/telemetry/Sparkline.tsx delete mode 100644 packages/ui/src/components/telemetry/TelemetryCard.tsx delete mode 100644 packages/ui/src/components/telemetry/TelemetryGrid.tsx delete mode 100644 packages/ui/src/components/telemetry/TelemetryStrip.tsx delete mode 100644 packages/ui/src/components/telemetry/index.ts delete mode 100644 packages/ui/src/content-boundary.test.ts delete mode 100644 packages/ui/src/css.d.ts delete mode 100644 packages/ui/src/fixtures.ts delete mode 100644 packages/ui/src/format.ts delete mode 100644 packages/ui/src/index.ts delete mode 100644 packages/ui/src/primitives/alert.tsx delete mode 100644 packages/ui/src/primitives/badge.tsx delete mode 100644 packages/ui/src/primitives/button.tsx delete mode 100644 packages/ui/src/primitives/card.tsx delete mode 100644 packages/ui/src/primitives/progress.tsx delete mode 100644 packages/ui/src/primitives/separator.tsx delete mode 100644 packages/ui/src/primitives/skeleton.tsx delete mode 100644 packages/ui/src/stories/Badge.stories.tsx delete mode 100644 packages/ui/src/stories/Button.stories.tsx delete mode 100644 packages/ui/src/stories/CornerBracketFrame.stories.tsx delete mode 100644 packages/ui/src/stories/DashboardFrame.stories.tsx delete mode 100644 packages/ui/src/stories/DashboardHeader.stories.tsx delete mode 100644 packages/ui/src/stories/DashboardOnePager.stories.tsx delete mode 100644 packages/ui/src/stories/DiagonalStripeField.stories.tsx delete mode 100644 packages/ui/src/stories/FooterCell.stories.tsx delete mode 100644 packages/ui/src/stories/FooterStatusCell.stories.tsx delete mode 100644 packages/ui/src/stories/GridFrame.stories.tsx delete mode 100644 packages/ui/src/stories/IconButton.stories.tsx delete mode 100644 packages/ui/src/stories/IconGlyph.stories.tsx delete mode 100644 packages/ui/src/stories/LineChart.stories.tsx delete mode 100644 packages/ui/src/stories/ModuleCard.stories.tsx delete mode 100644 packages/ui/src/stories/Panel.stories.tsx delete mode 100644 packages/ui/src/stories/ProgressMeter.stories.tsx delete mode 100644 packages/ui/src/stories/ScanlineField.stories.tsx delete mode 100644 packages/ui/src/stories/Separator.stories.tsx delete mode 100644 packages/ui/src/stories/ServiceGroupPanel.stories.tsx delete mode 100644 packages/ui/src/stories/ServicePanel.stories.tsx delete mode 100644 packages/ui/src/stories/ServiceRow.stories.tsx delete mode 100644 packages/ui/src/stories/SignalTrace.stories.tsx delete mode 100644 packages/ui/src/stories/Sparkline.stories.tsx delete mode 100644 packages/ui/src/stories/StatusBadge.stories.tsx delete mode 100644 packages/ui/src/stories/StatusStrip.stories.tsx delete mode 100644 packages/ui/src/stories/SystemState.stories.tsx delete mode 100644 packages/ui/src/stories/TelemetryCard.stories.tsx delete mode 100644 packages/ui/src/stories/TelemetryGrid.stories.tsx delete mode 100644 packages/ui/src/stories/TelemetryStrip.stories.tsx delete mode 100644 packages/ui/src/stories/ThemeToggle.stories.tsx delete mode 100644 packages/ui/src/stories/WeatherModule.stories.tsx delete mode 100644 packages/ui/src/stories/story-data.ts delete mode 100644 packages/ui/src/storybook.test.ts delete mode 100644 packages/ui/src/styles.css delete mode 100644 packages/ui/src/theme.test.ts delete mode 100644 packages/ui/src/theme.ts delete mode 100644 packages/ui/src/tokens.css delete mode 100644 packages/ui/src/types.ts delete mode 100644 packages/ui/src/utils.ts delete mode 100644 packages/ui/tsconfig.build.json delete mode 100644 packages/ui/tsconfig.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..32d1c14 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "packages/ui"] + path = packages/ui + url = ssh://git@git.dimensionlab.net/vince/dimensionlab-ui.git + branch = main diff --git a/packages/ui b/packages/ui new file mode 160000 index 0000000..000a3ac --- /dev/null +++ b/packages/ui @@ -0,0 +1 @@ +Subproject commit 000a3ac643f33328ad9fa84d9213d7f77ddcae6e diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts deleted file mode 100644 index 34237be..0000000 --- a/packages/ui/.storybook/main.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { StorybookConfig } from "@storybook/react-vite"; - -const config: StorybookConfig = { - stories: ["../src/**/*.stories.@(js|ts|tsx)"], - addons: [ - "@storybook/addon-a11y", - "@storybook/addon-vitest", - ], - framework: { - name: "@storybook/react-vite", - options: {}, - }, -}; - -export default config; diff --git a/packages/ui/.storybook/preview.ts b/packages/ui/.storybook/preview.ts deleted file mode 100644 index 87df132..0000000 --- a/packages/ui/.storybook/preview.ts +++ /dev/null @@ -1,51 +0,0 @@ -import "../src/styles.css"; -import type { Preview } from "@storybook/react-vite"; - -const preview: Preview = { - decorators: [ - (Story, context) => { - const theme = context.globals.theme === "light" ? "light" : "dark"; - - if (typeof document !== "undefined") { - document.documentElement.setAttribute("data-ui-theme", theme); - } - - return Story(); - }, - ], - globalTypes: { - theme: { - description: "Dashboard component theme", - defaultValue: "dark", - toolbar: { - title: "Theme", - icon: "circlehollow", - items: [ - { value: "dark", title: "Dark" }, - { value: "light", title: "Light" }, - ], - dynamicTitle: true, - }, - }, - }, - parameters: { - backgrounds: { - default: "canvas", - values: [ - { name: "canvas", value: "#0b0f0d" }, - { name: "raised", value: "#151d18" }, - { name: "light canvas", value: "#eef2e7" }, - { name: "light raised", value: "#f1f5ea" }, - ], - }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - layout: "fullscreen", - }, -}; - -export default preview; diff --git a/packages/ui/components.json b/packages/ui/components.json deleted file mode 100644 index d951369..0000000 --- a/packages/ui/components.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "radix-nova", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/styles.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "rtl": false, - "aliases": { - "components": "src/components", - "utils": "src/utils", - "ui": "src/primitives", - "lib": "src", - "hooks": "src/hooks" - }, - "menuColor": "default", - "menuAccent": "subtle", - "registries": {} -} diff --git a/packages/ui/package.json b/packages/ui/package.json deleted file mode 100644 index a30db52..0000000 --- a/packages/ui/package.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "name": "@dimensionlab/ui", - "version": "0.0.1", - "private": true, - "type": "module", - "sideEffects": ["*.css", "**/*.css"], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "development": "./src/index.ts", - "default": "./dist/index.js" - }, - "./foundation": { - "types": "./dist/components/foundation/index.d.ts", - "development": "./src/components/foundation/index.ts", - "default": "./dist/components/foundation/index.js" - }, - "./frames": { - "types": "./dist/components/frames/index.d.ts", - "development": "./src/components/frames/index.ts", - "default": "./dist/components/frames/index.js" - }, - "./operations": { - "types": "./dist/components/operations/index.d.ts", - "development": "./src/components/operations/index.ts", - "default": "./dist/components/operations/index.js" - }, - "./telemetry": { - "types": "./dist/components/telemetry/index.d.ts", - "development": "./src/components/telemetry/index.ts", - "default": "./dist/components/telemetry/index.js" - }, - "./styles.css": { - "development": "./src/styles.css", - "default": "./dist/styles.css" - }, - "./tokens.css": { - "development": "./src/tokens.css", - "default": "./dist/tokens.css" - } - }, - "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/components && cp src/styles.css dist/styles.css && cp src/tokens.css dist/tokens.css && cp src/components/styles.css dist/components/styles.css", - "check": "tsc --noEmit", - "test": "vitest run", - "test:unit": "vitest run", - "storybook": "storybook dev -p 6006 --host 0.0.0.0", - "build-storybook": "storybook build" - }, - "dependencies": { - "@fontsource-variable/geist": "^5.2.9", - "@iconify/react": "^6.0.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^1.21.0", - "radix-ui": "^1.6.0", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0", - "uplot": "^1.6.32" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@storybook/addon-a11y": "^10.4.6", - "@storybook/addon-vitest": "^10.4.6", - "@storybook/react-vite": "^10.4.6", - "@types/bun": "^1.3.14", - "@types/node": "^25.9.3", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "bun-types": "^1.3.14", - "storybook": "^10.4.6", - "typescript": "^6.0.3", - "vite": "^8.0.16", - "vitest": "^4.1.9" - } -} diff --git a/packages/ui/src/components/foundation/Badge.tsx b/packages/ui/src/components/foundation/Badge.tsx deleted file mode 100644 index 5bdee48..0000000 --- a/packages/ui/src/components/foundation/Badge.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { UiSeverity } from "../../types"; -import { StatusBadge } from "./StatusBadge"; - -export interface BadgeProps { - label: string; - severity?: UiSeverity; -} - -export function Badge({ label, severity = "neutral" }: BadgeProps) { - return ; -} diff --git a/packages/ui/src/components/foundation/Button.tsx b/packages/ui/src/components/foundation/Button.tsx deleted file mode 100644 index 8ae3b4e..0000000 --- a/packages/ui/src/components/foundation/Button.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { ButtonHTMLAttributes } from "react"; -import { IconGlyph } from "./IconGlyph"; - -export interface ButtonProps - extends Omit, "type"> { - label: string; - variant?: "primary" | "secondary" | "danger" | "ghost"; - size?: "default" | "compact"; - icon?: string; - loading?: boolean; - type?: "button" | "submit" | "reset"; -} - -export function Button({ - label, - variant = "primary", - size = "default", - icon, - disabled = false, - loading = false, - type = "button", - className = "", - ...buttonProps -}: ButtonProps) { - return ( - - ); -} diff --git a/packages/ui/src/components/foundation/IconButton.tsx b/packages/ui/src/components/foundation/IconButton.tsx deleted file mode 100644 index 92fcb46..0000000 --- a/packages/ui/src/components/foundation/IconButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { ButtonHTMLAttributes } from "react"; -import { IconGlyph } from "./IconGlyph"; - -export interface IconButtonProps - extends Omit, "type"> { - icon: string; - label: string; - active?: boolean; - type?: "button" | "submit" | "reset"; -} - -export function IconButton({ - icon, - label, - active = false, - disabled = false, - type = "button", - className = "", - ...buttonProps -}: IconButtonProps) { - return ( - - ); -} diff --git a/packages/ui/src/components/foundation/IconGlyph.tsx b/packages/ui/src/components/foundation/IconGlyph.tsx deleted file mode 100644 index d21feb1..0000000 --- a/packages/ui/src/components/foundation/IconGlyph.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Icon } from "@iconify/react"; - -export interface IconGlyphProps { - name?: string; - label?: string; - size?: "sm" | "md" | "lg"; -} - -export function IconGlyph({ name, label, size = "md" }: IconGlyphProps) { - return ( - - {name ? : null} - - ); -} diff --git a/packages/ui/src/components/foundation/ProgressMeter.tsx b/packages/ui/src/components/foundation/ProgressMeter.tsx deleted file mode 100644 index c3f3aad..0000000 --- a/packages/ui/src/components/foundation/ProgressMeter.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { CSSProperties } from "react"; -import { clampPercent } from "../../format"; -import type { UiSeverity } from "../../types"; - -export interface ProgressMeterProps { - value?: number; - severity?: UiSeverity; - label?: string; -} - -export function ProgressMeter({ - value, - severity = "neutral", - label = "Progress", -}: ProgressMeterProps) { - const progress = value === undefined ? null : clampPercent(value); - - return ( -
- {progress !== null ? ( - - ) : null} -
- ); -} diff --git a/packages/ui/src/components/foundation/Separator.tsx b/packages/ui/src/components/foundation/Separator.tsx deleted file mode 100644 index ca1a955..0000000 --- a/packages/ui/src/components/foundation/Separator.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export interface SeparatorProps { - orientation?: "horizontal" | "vertical"; - dense?: boolean; -} - -export function Separator({ - orientation = "horizontal", - dense = false, -}: SeparatorProps) { - return ( -
- ); -} diff --git a/packages/ui/src/components/foundation/StatusBadge.tsx b/packages/ui/src/components/foundation/StatusBadge.tsx deleted file mode 100644 index d2b01b3..0000000 --- a/packages/ui/src/components/foundation/StatusBadge.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { UiSeverity } from "../../types"; - -export interface StatusBadgeProps { - label: string; - severity?: UiSeverity; -} - -export function StatusBadge({ label, severity = "neutral" }: StatusBadgeProps) { - return ( - - {label} - - ); -} diff --git a/packages/ui/src/components/foundation/ThemeToggle.tsx b/packages/ui/src/components/foundation/ThemeToggle.tsx deleted file mode 100644 index 5d83031..0000000 --- a/packages/ui/src/components/foundation/ThemeToggle.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { UiTheme } from "../../theme"; -import { getNextUiTheme } from "../../theme"; -import { IconGlyph } from "./IconGlyph"; - -export interface ThemeToggleProps { - theme: UiTheme; - onThemeChange: (theme: UiTheme) => void; -} - -export function ThemeToggle({ theme, onThemeChange }: ThemeToggleProps) { - const nextTheme = getNextUiTheme(theme); - - return ( - - ); -} diff --git a/packages/ui/src/components/foundation/index.ts b/packages/ui/src/components/foundation/index.ts deleted file mode 100644 index b82cd47..0000000 --- a/packages/ui/src/components/foundation/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./Badge"; -export * from "./Button"; -export * from "./IconButton"; -export * from "./IconGlyph"; -export * from "./ProgressMeter"; -export * from "./Separator"; -export * from "./StatusBadge"; -export * from "./ThemeToggle"; diff --git a/packages/ui/src/components/frames/CornerBracketFrame.tsx b/packages/ui/src/components/frames/CornerBracketFrame.tsx deleted file mode 100644 index 1dfd4f7..0000000 --- a/packages/ui/src/components/frames/CornerBracketFrame.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export interface CornerBracketFrameProps { - density?: "regular" | "tight"; - size?: "sm" | "md" | "lg"; - tone?: "neutral" | "accent" | "danger"; -} - -export function CornerBracketFrame({ - density = "regular", - size = "md", - tone = "neutral", -}: CornerBracketFrameProps) { - return ( - - ); -} diff --git a/packages/ui/src/components/frames/DashboardFrame.tsx b/packages/ui/src/components/frames/DashboardFrame.tsx deleted file mode 100644 index 08029cf..0000000 --- a/packages/ui/src/components/frames/DashboardFrame.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useId } from "react"; -import type { ReactNode } from "react"; -import type { UiDashboardPreview } from "../../types"; -import { ModuleCard } from "./ModuleCard"; -import { ServicePanel } from "../operations/ServicePanel"; -import { StatusStrip } from "../operations/StatusStrip"; -import { TelemetryGrid } from "../telemetry/TelemetryGrid"; - -export interface DashboardFrameProps { - actions?: ReactNode; - dashboard: UiDashboardPreview; - titleId?: string; -} - -export function DashboardFrame({ - actions, - dashboard, - titleId, -}: DashboardFrameProps) { - const generatedTitleId = useId(); - const resolvedTitleId = titleId || `${generatedTitleId}-title`; - - return ( -
-
-
-
- {dashboard.eyebrow ?

{dashboard.eyebrow}

: null} -

{dashboard.title}

- {dashboard.subtitle ? {dashboard.subtitle} : null} -
- {actions ? ( -
{actions}
- ) : null} -
- {dashboard.modules.length ? ( -
- {dashboard.modules.map((module) => ( - - ))} -
- ) : null} -
- - - -
- {dashboard.serviceGroups.map((group) => ( - - ))} -
- - -
- ); -} diff --git a/packages/ui/src/components/frames/DashboardHeader.tsx b/packages/ui/src/components/frames/DashboardHeader.tsx deleted file mode 100644 index 3f418fa..0000000 --- a/packages/ui/src/components/frames/DashboardHeader.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useId } from "react"; -import type { UiModuleBlock } from "../../types"; -import { ModuleCard } from "./ModuleCard"; - -export interface DashboardHeaderProps { - title: string; - subtitle?: string; - eyebrow?: string; - module?: UiModuleBlock; -} - -export function DashboardHeader({ - title, - subtitle, - eyebrow, - module, -}: DashboardHeaderProps) { - const titleId = useId(); - - return ( -
-
- {eyebrow ?

{eyebrow}

: null} -

{title}

- {subtitle ? {subtitle} : null} -
- {module ? : null} -
- ); -} diff --git a/packages/ui/src/components/frames/DiagonalStripeField.tsx b/packages/ui/src/components/frames/DiagonalStripeField.tsx deleted file mode 100644 index b4c60f8..0000000 --- a/packages/ui/src/components/frames/DiagonalStripeField.tsx +++ /dev/null @@ -1,24 +0,0 @@ -export interface DiagonalStripeFieldProps { - density?: "open" | "regular" | "tight"; - direction?: "forward" | "backward"; - size?: "sm" | "md" | "lg"; - tone?: "neutral" | "accent" | "warning" | "danger"; -} - -export function DiagonalStripeField({ - density = "regular", - direction = "forward", - size = "md", - tone = "accent", -}: DiagonalStripeFieldProps) { - return ( -