refactor(ui): organize component source tree #35

Merged
vince merged 2 commits from codex/turbo-ui-filetree into main 2026-06-20 06:52:12 +02:00
66 changed files with 249 additions and 160 deletions

View file

@ -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

View file

@ -1,4 +1,4 @@
import type { UiSeverity } from "../types";
import type { UiSeverity } from "../../types";
import { StatusBadge } from "./StatusBadge";
export interface BadgeProps {

View file

@ -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;

View file

@ -1,4 +1,4 @@
import type { UiSeverity } from "../types";
import type { UiSeverity } from "../../types";
export interface StatusBadgeProps {
label: string;

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -1,4 +1,4 @@
import type { UiStatusItem } from "../types";
import type { UiStatusItem } from "../../types";
export interface FooterCellProps {
item: UiStatusItem;

View file

@ -1,4 +1,4 @@
import type { UiStatusItem } from "../types";
import type { UiStatusItem } from "../../types";
import { FooterCell } from "./FooterCell";
export function FooterStatusCell({ item }: { item: UiStatusItem }) {

View file

@ -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;

View file

@ -1,4 +1,4 @@
import type { UiServiceGroup } from "../types";
import type { UiServiceGroup } from "../../types";
import { ServicePanel } from "./ServicePanel";
export function ServiceGroupPanel({ group }: { group: UiServiceGroup }) {

View file

@ -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";

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 <ModuleCard module={module} />;

View file

@ -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", () => {

View file

@ -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[];

View file

@ -1,4 +1,4 @@
import type { UiSeverity } from "../types";
import type { UiSeverity } from "../../types";
export interface SparklineProps {
values?: number[];

View file

@ -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 {

View file

@ -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 {

View file

@ -1,4 +1,4 @@
import type { UiTelemetryCard } from "../types";
import type { UiTelemetryCard } from "../../types";
import { TelemetryGrid } from "./TelemetryGrid";
export function TelemetryStrip({ cards }: { cards: UiTelemetryCard[] }) {

View file

@ -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();

View file

@ -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,

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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",

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 = {

View file

@ -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",

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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",

View file

@ -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 = {

View file

@ -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,39 +9,13 @@ const packageRoot = existsSync(join(root, "packages/ui/package.json"))
const componentsDir = join(packageRoot, "src/components");
const storiesDir = join(packageRoot, "src/stories");
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",
@ -62,17 +36,26 @@ 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(
join(componentsDir, "DashboardFrame.tsx"),
"utf8",
);
const dashboardFrame = readFileSync(requiredComponentPath("DashboardFrame"), "utf8");
expect(packageStyles).toContain('./components/styles.css');
expect(dashboardFrame).not.toContain('./styles.css');
@ -87,12 +70,33 @@ 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 = 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}";`,
);
}
});
@ -114,9 +118,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,14 +133,19 @@ 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"');
}
});
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(componentNames.has(component)).toBe(false);
expect(existsSync(join(componentsDir, `${component}.tsx`))).toBe(false);
expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false);
}
@ -153,3 +160,71 @@ describe("Storybook inventory", () => {
expect(legacyStories).toEqual([]);
});
});
function requiredComponentPath(component: string): string {
const componentPath = componentInventory().find(
(entry) => entry.name === component,
)?.path;
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();
}