From 32ad2cebe474535a44841f128398b87a4a4b2e44 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 06:43:09 +0200 Subject: [PATCH 1/2] 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 2/2] 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(); +}