From 3e46a2c0cc853bb4023f2fbd0c9c8c47b3e72f5d Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 07:01:15 +0200 Subject: [PATCH 01/37] 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 ( -