refactor(ui): organize component source tree

This commit is contained in:
vince 2026-06-20 06:43:09 +02:00
parent 8927f2ab8a
commit 32ad2cebe4
66 changed files with 194 additions and 124 deletions

View file

@ -16,7 +16,8 @@ validated dashboard model state inside the web app.
- `packages/dashboard-model`: reusable dashboard schema, validation, and - `packages/dashboard-model`: reusable dashboard schema, validation, and
generic model fixtures shared by apps and tooling. generic model fixtures shared by apps and tooling.
- `packages/ui`: reusable dashboard React components, design tokens, - `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. - `docs/superpowers`: migration specs and execution plans used for this repo.
## Development ## Development

View file

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

View file

@ -1,6 +1,6 @@
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { clampPercent } from "../format"; import { clampPercent } from "../../format";
import type { UiSeverity } from "../types"; import type { UiSeverity } from "../../types";
export interface ProgressMeterProps { export interface ProgressMeterProps {
value?: number; value?: number;

View file

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

View file

@ -1,5 +1,5 @@
import type { UiTheme } from "../theme"; import type { UiTheme } from "../../theme";
import { getNextUiTheme } from "../theme"; import { getNextUiTheme } from "../../theme";
import { IconGlyph } from "./IconGlyph"; import { IconGlyph } from "./IconGlyph";
export interface ThemeToggleProps { export interface ThemeToggleProps {

View file

@ -1,10 +1,10 @@
import { useId } from "react"; import { useId } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { UiDashboardPreview } from "../types"; import type { UiDashboardPreview } from "../../types";
import { ModuleCard } from "./ModuleCard"; import { ModuleCard } from "./ModuleCard";
import { ServicePanel } from "./ServicePanel"; import { ServicePanel } from "../operations/ServicePanel";
import { StatusStrip } from "./StatusStrip"; import { StatusStrip } from "../operations/StatusStrip";
import { TelemetryGrid } from "./TelemetryGrid"; import { TelemetryGrid } from "../telemetry/TelemetryGrid";
export interface DashboardFrameProps { export interface DashboardFrameProps {
actions?: ReactNode; actions?: ReactNode;

View file

@ -1,5 +1,5 @@
import { useId } from "react"; import { useId } from "react";
import type { UiModuleBlock } from "../types"; import type { UiModuleBlock } from "../../types";
import { ModuleCard } from "./ModuleCard"; import { ModuleCard } from "./ModuleCard";
export interface DashboardHeaderProps { export interface DashboardHeaderProps {

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { useId } from "react"; import { useId } from "react";
import type { UiModuleBlock } from "../types"; import type { UiModuleBlock } from "../../types";
import { IconGlyph } from "./IconGlyph"; import { IconGlyph } from "../foundation/IconGlyph";
export interface ModuleCardProps { export interface ModuleCardProps {
module: UiModuleBlock; module: UiModuleBlock;

View file

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

View file

@ -1,5 +1,5 @@
import type { UiServiceGroup } from "../types"; import type { UiServiceGroup } from "../../types";
import { Panel } from "./Panel"; import { Panel } from "../frames/Panel";
import { ServiceRow } from "./ServiceRow"; import { ServiceRow } from "./ServiceRow";
import { StatusStrip } from "./StatusStrip"; import { StatusStrip } from "./StatusStrip";

View file

@ -1,6 +1,6 @@
import type { UiServiceRow } from "../types"; import type { UiServiceRow } from "../../types";
import { IconGlyph } from "./IconGlyph"; import { IconGlyph } from "../foundation/IconGlyph";
import { StatusBadge } from "./StatusBadge"; import { StatusBadge } from "../foundation/StatusBadge";
export interface ServiceRowProps { export interface ServiceRowProps {
service: UiServiceRow; service: UiServiceRow;

View file

@ -1,5 +1,5 @@
import type { UiStatusItem } from "../types"; import type { UiStatusItem } from "../../types";
import { FooterCell } from "./FooterCell"; import { FooterCell } from "../frames/FooterCell";
export interface StatusStripProps { export interface StatusStripProps {
id?: string; id?: string;

View file

@ -1,5 +1,5 @@
import type { UiSeverity } from "../types"; import type { UiSeverity } from "../../types";
import { IconGlyph } from "./IconGlyph"; import { IconGlyph } from "../foundation/IconGlyph";
export interface SystemStateProps { export interface SystemStateProps {
title: string; title: string;

View file

@ -1,5 +1,5 @@
import type { UiModuleBlock } from "../types"; import type { UiModuleBlock } from "../../types";
import { ModuleCard } from "./ModuleCard"; import { ModuleCard } from "../frames/ModuleCard";
export function WeatherModule({ module }: { module: UiModuleBlock }) { export function WeatherModule({ module }: { module: UiModuleBlock }) {
return <ModuleCard module={module} />; return <ModuleCard module={module} />;

View file

@ -1,14 +1,16 @@
import { renderToString } from "react-dom/server"; import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { Button } from "./Button"; import {
import { DashboardFrame } from "./DashboardFrame"; Button,
import { FooterCell } from "./FooterCell"; DashboardFrame,
import { IconButton } from "./IconButton"; FooterCell,
import { ServiceRow } from "./ServiceRow"; IconButton,
import { StatusStrip } from "./StatusStrip"; ServiceRow,
import { TelemetryCard } from "./TelemetryCard"; StatusStrip,
import { TelemetryGrid } from "./TelemetryGrid"; TelemetryCard,
import { ThemeToggle } from "./ThemeToggle"; TelemetryGrid,
ThemeToggle,
} from "../index";
import { dashboardPreviewFixtures } from "../fixtures"; import { dashboardPreviewFixtures } from "../fixtures";
describe("dashboard UI components", () => { describe("dashboard UI components", () => {

View file

@ -1,6 +1,6 @@
import "uplot/dist/uPlot.min.css"; import "uplot/dist/uPlot.min.css";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import type { UiSeverity } from "../types"; import type { UiSeverity } from "../../types";
export interface LineChartProps { export interface LineChartProps {
values?: number[]; values?: number[];

View file

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

View file

@ -1,7 +1,7 @@
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { clampPercent, formatMetricValue } from "../format"; import { clampPercent, formatMetricValue } from "../../format";
import type { UiTelemetryCard } from "../types"; import type { UiTelemetryCard } from "../../types";
import { IconGlyph } from "./IconGlyph"; import { IconGlyph } from "../foundation/IconGlyph";
import { LineChart } from "./LineChart"; import { LineChart } from "./LineChart";
export interface TelemetryCardProps { export interface TelemetryCardProps {

View file

@ -1,5 +1,5 @@
import { useId } from "react"; import { useId } from "react";
import type { UiTelemetryCard } from "../types"; import type { UiTelemetryCard } from "../../types";
import { TelemetryCard } from "./TelemetryCard"; import { TelemetryCard } from "./TelemetryCard";
export interface TelemetryGridProps { export interface TelemetryGridProps {

View file

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

View file

@ -26,15 +26,28 @@ const forbiddenTerms = [
describe("UI package content boundary", () => { describe("UI package content boundary", () => {
test("contains the reusable dashboard component inventory", () => { test("contains the reusable dashboard component inventory", () => {
expect(existsSync(join(uiSourceRoot, "index.ts"))).toBe(true); expect(existsSync(join(uiSourceRoot, "index.ts"))).toBe(true);
expect(existsSync(join(uiSourceRoot, "components/DashboardFrame.tsx"))).toBe( expect(
true, existsSync(join(uiSourceRoot, "components/frames/DashboardFrame.tsx")),
); ).toBe(true);
expect(existsSync(join(uiSourceRoot, "components/ThemeToggle.tsx"))).toBe( expect(
true, existsSync(join(uiSourceRoot, "components/foundation/ThemeToggle.tsx")),
); ).toBe(true);
expect(existsSync(join(uiSourceRoot, "styles.css"))).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", () => { test("keeps environment-specific content out of reusable UI source", () => {
const source = readUiSource(uiSourceRoot).toLowerCase(); const source = readUiSource(uiSourceRoot).toLowerCase();

View file

@ -1,33 +1,33 @@
export { Badge } from "./components/Badge"; export { Badge } from "./components/foundation/Badge";
export { Button } from "./components/Button"; export { Button } from "./components/foundation/Button";
export { CornerBracketFrame } from "./components/CornerBracketFrame"; export { IconGlyph } from "./components/foundation/IconGlyph";
export { DashboardHeader } from "./components/DashboardHeader"; export { IconButton } from "./components/foundation/IconButton";
export { DashboardFrame } from "./components/DashboardFrame"; export { ProgressMeter } from "./components/foundation/ProgressMeter";
export { DiagonalStripeField } from "./components/DiagonalStripeField"; export { Separator } from "./components/foundation/Separator";
export { FooterCell } from "./components/FooterCell"; export { StatusBadge } from "./components/foundation/StatusBadge";
export { FooterStatusCell } from "./components/FooterStatusCell"; export { ThemeToggle } from "./components/foundation/ThemeToggle";
export { GridFrame } from "./components/GridFrame"; export { CornerBracketFrame } from "./components/frames/CornerBracketFrame";
export { IconGlyph } from "./components/IconGlyph"; export { DashboardHeader } from "./components/frames/DashboardHeader";
export { IconButton } from "./components/IconButton"; export { DashboardFrame } from "./components/frames/DashboardFrame";
export { LineChart } from "./components/LineChart"; export { DiagonalStripeField } from "./components/frames/DiagonalStripeField";
export { ModuleCard } from "./components/ModuleCard"; export { FooterCell } from "./components/frames/FooterCell";
export { Panel } from "./components/Panel"; export { FooterStatusCell } from "./components/frames/FooterStatusCell";
export { ProgressMeter } from "./components/ProgressMeter"; export { GridFrame } from "./components/frames/GridFrame";
export { Separator } from "./components/Separator"; export { ModuleCard } from "./components/frames/ModuleCard";
export { ServiceGroupPanel } from "./components/ServiceGroupPanel"; export { Panel } from "./components/frames/Panel";
export { ServicePanel } from "./components/ServicePanel"; export { ScanlineField } from "./components/frames/ScanlineField";
export { ServiceRow } from "./components/ServiceRow"; export { ServiceGroupPanel } from "./components/operations/ServiceGroupPanel";
export { ScanlineField } from "./components/ScanlineField"; export { ServicePanel } from "./components/operations/ServicePanel";
export { SignalTrace } from "./components/SignalTrace"; export { ServiceRow } from "./components/operations/ServiceRow";
export { Sparkline } from "./components/Sparkline"; export { StatusStrip } from "./components/operations/StatusStrip";
export { StatusBadge } from "./components/StatusBadge"; export { SystemState } from "./components/operations/SystemState";
export { StatusStrip } from "./components/StatusStrip"; export { WeatherModule } from "./components/operations/WeatherModule";
export { SystemState } from "./components/SystemState"; export { LineChart } from "./components/telemetry/LineChart";
export { TelemetryCard } from "./components/TelemetryCard"; export { SignalTrace } from "./components/telemetry/SignalTrace";
export { TelemetryGrid } from "./components/TelemetryGrid"; export { Sparkline } from "./components/telemetry/Sparkline";
export { TelemetryStrip } from "./components/TelemetryStrip"; export { TelemetryCard } from "./components/telemetry/TelemetryCard";
export { ThemeToggle } from "./components/ThemeToggle"; export { TelemetryGrid } from "./components/telemetry/TelemetryGrid";
export { WeatherModule } from "./components/WeatherModule"; export { TelemetryStrip } from "./components/telemetry/TelemetryStrip";
export { dashboardPreviewFixtures } from "./fixtures"; export { dashboardPreviewFixtures } from "./fixtures";
export { export {
getNextUiTheme, getNextUiTheme,

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { Badge } from "../components/Badge"; import { Badge } from "../index";
const meta = { const meta = {
title: "UI/Badge", title: "UI/Badge",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "../components/Button"; import { Button } from "../index";
const meta = { const meta = {
title: "UI/Button", title: "UI/Button",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { CornerBracketFrame } from "../components/CornerBracketFrame"; import { CornerBracketFrame } from "../index";
const meta = { const meta = {
title: "UI/CornerBracketFrame", title: "UI/CornerBracketFrame",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { DashboardFrame } from "../components/DashboardFrame"; import { DashboardFrame } from "../index";
import { fullCompositionDashboard, secondaryDashboard } from "./story-data"; import { fullCompositionDashboard, secondaryDashboard } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { DashboardHeader } from "../components/DashboardHeader"; import { DashboardHeader } from "../index";
import { moduleBlocks } from "./story-data"; import { moduleBlocks } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { DashboardFrame } from "../components/DashboardFrame"; import { DashboardFrame } from "../index";
import { fullCompositionDashboard } from "./story-data"; import { fullCompositionDashboard } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { DiagonalStripeField } from "../components/DiagonalStripeField"; import { DiagonalStripeField } from "../index";
const meta = { const meta = {
title: "UI/DiagonalStripeField", title: "UI/DiagonalStripeField",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { FooterCell } from "../components/FooterCell"; import { FooterCell } from "../index";
import { statusItems } from "./story-data"; import { statusItems } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { FooterStatusCell } from "../components/FooterStatusCell"; import { FooterStatusCell } from "../index";
import { statusItems } from "./story-data"; import { statusItems } from "./story-data";
const meta = { const meta = {

View file

@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { GridFrame } from "../components/GridFrame"; import { GridFrame, ModuleCard } from "../index";
import { ModuleCard } from "../components/ModuleCard";
import { moduleBlocks } from "./story-data"; import { moduleBlocks } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { IconButton } from "../components/IconButton"; import { IconButton } from "../index";
const meta = { const meta = {
title: "UI/IconButton", title: "UI/IconButton",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { IconGlyph } from "../components/IconGlyph"; import { IconGlyph } from "../index";
const meta = { const meta = {
title: "UI/IconGlyph", title: "UI/IconGlyph",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { LineChart } from "../components/LineChart"; import { LineChart } from "../index";
const meta = { const meta = {
title: "UI/LineChart", title: "UI/LineChart",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ModuleCard } from "../components/ModuleCard"; import { ModuleCard } from "../index";
import { moduleBlocks } from "./story-data"; import { moduleBlocks } from "./story-data";
const meta = { const meta = {

View file

@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { Panel } from "../components/Panel"; import { Panel, ServiceRow } from "../index";
import { ServiceRow } from "../components/ServiceRow";
import { serviceRows } from "./story-data"; import { serviceRows } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ProgressMeter } from "../components/ProgressMeter"; import { ProgressMeter } from "../index";
const meta = { const meta = {
title: "UI/ProgressMeter", title: "UI/ProgressMeter",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ScanlineField } from "../components/ScanlineField"; import { ScanlineField } from "../index";
const meta = { const meta = {
title: "UI/ScanlineField", title: "UI/ScanlineField",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { Separator } from "../components/Separator"; import { Separator } from "../index";
const meta = { const meta = {
title: "UI/Separator", title: "UI/Separator",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ServiceGroupPanel } from "../components/ServiceGroupPanel"; import { ServiceGroupPanel } from "../index";
import { serviceGroups } from "./story-data"; import { serviceGroups } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ServicePanel } from "../components/ServicePanel"; import { ServicePanel } from "../index";
import { serviceGroups } from "./story-data"; import { serviceGroups } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ServiceRow } from "../components/ServiceRow"; import { ServiceRow } from "../index";
import { serviceRows } from "./story-data"; import { serviceRows } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { SignalTrace } from "../components/SignalTrace"; import { SignalTrace } from "../index";
const meta = { const meta = {
title: "UI/SignalTrace", title: "UI/SignalTrace",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { Sparkline } from "../components/Sparkline"; import { Sparkline } from "../index";
const meta = { const meta = {
title: "UI/Sparkline", title: "UI/Sparkline",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { StatusBadge } from "../components/StatusBadge"; import { StatusBadge } from "../index";
const meta = { const meta = {
title: "UI/StatusBadge", title: "UI/StatusBadge",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { StatusStrip } from "../components/StatusStrip"; import { StatusStrip } from "../index";
import { mixedStatusStrip } from "./story-data"; import { mixedStatusStrip } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { SystemState } from "../components/SystemState"; import { SystemState } from "../index";
const meta = { const meta = {
title: "UI/SystemState", title: "UI/SystemState",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { TelemetryCard } from "../components/TelemetryCard"; import { TelemetryCard } from "../index";
import { telemetryCards } from "./story-data"; import { telemetryCards } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { TelemetryGrid } from "../components/TelemetryGrid"; import { TelemetryGrid } from "../index";
import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data"; import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { TelemetryStrip } from "../components/TelemetryStrip"; import { TelemetryStrip } from "../index";
import { eightTelemetryCards } from "./story-data"; import { eightTelemetryCards } from "./story-data";
const meta = { const meta = {

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { ThemeToggle } from "../components/ThemeToggle"; import { ThemeToggle } from "../index";
const meta = { const meta = {
title: "UI/ThemeToggle", title: "UI/ThemeToggle",

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { WeatherModule } from "../components/WeatherModule"; import { WeatherModule } from "../index";
import { moduleBlocks } from "./story-data"; import { moduleBlocks } from "./story-data";
const meta = { const meta = {

View file

@ -9,6 +9,56 @@ const packageRoot = existsSync(join(root, "packages/ui/package.json"))
const componentsDir = join(packageRoot, "src/components"); const componentsDir = join(packageRoot, "src/components");
const storiesDir = join(packageRoot, "src/stories"); 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<string, string> = new Map(
Object.entries(componentDomains).flatMap(([domain, components]) =>
components.map((component) => [
component,
join(componentsDir, domain, `${component}.tsx`),
]),
),
);
const requiredStoryFiles = [ const requiredStoryFiles = [
"Badge.stories.tsx", "Badge.stories.tsx",
"Button.stories.tsx", "Button.stories.tsx",
@ -69,10 +119,7 @@ describe("Storybook inventory", () => {
test("loads dashboard component styles through the global app stylesheet", () => { test("loads dashboard component styles through the global app stylesheet", () => {
const packageStyles = readFileSync(join(packageRoot, "src/styles.css"), "utf8"); const packageStyles = readFileSync(join(packageRoot, "src/styles.css"), "utf8");
const dashboardFrame = readFileSync( const dashboardFrame = readFileSync(requiredComponentPath("DashboardFrame"), "utf8");
join(componentsDir, "DashboardFrame.tsx"),
"utf8",
);
expect(packageStyles).toContain('./components/styles.css'); expect(packageStyles).toContain('./components/styles.css');
expect(dashboardFrame).not.toContain('./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", () => { test("keeps component and story files paired as the UI inventory changes", () => {
const componentStoryFiles = readdirSync(componentsDir) const componentStoryFiles = [...componentFiles.keys()].map(
.filter((filename) => filename.endsWith(".tsx") && !filename.endsWith(".test.tsx")) (component) => `${component}.stories.tsx`,
.map((filename) => filename.replace(".tsx", ".stories.tsx")); );
for (const filename of componentStoryFiles) { 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);
@ -114,9 +161,7 @@ describe("Storybook inventory", () => {
"ScanlineField", "ScanlineField",
"SignalTrace", "SignalTrace",
]) { ]) {
expect(existsSync(join(componentsDir, `${component}.tsx`)), `${component} is missing`).toBe( expect(existsSync(requiredComponentPath(component)), `${component} is missing`).toBe(true);
true,
);
expect( expect(
existsSync(join(storiesDir, `${component}.stories.tsx`)), existsSync(join(storiesDir, `${component}.stories.tsx`)),
`${component}.stories.tsx is missing`, `${component}.stories.tsx is missing`,
@ -131,7 +176,7 @@ describe("Storybook inventory", () => {
"ScanlineField", "ScanlineField",
"SignalTrace", "SignalTrace",
]) { ]) {
const source = readFileSync(join(componentsDir, `${component}.tsx`), "utf8"); const source = readFileSync(requiredComponentPath(component), "utf8");
expect(source).toContain('aria-hidden="true"'); expect(source).toContain('aria-hidden="true"');
} }
@ -139,6 +184,7 @@ describe("Storybook inventory", () => {
test("does not add deferred form/navigation primitives", () => { test("does not add deferred form/navigation primitives", () => {
for (const component of ["Input", "ToggleGroup", "ScrollArea"]) { for (const component of ["Input", "ToggleGroup", "ScrollArea"]) {
expect(componentFiles.has(component)).toBe(false);
expect(existsSync(join(componentsDir, `${component}.tsx`))).toBe(false); expect(existsSync(join(componentsDir, `${component}.tsx`))).toBe(false);
expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false); expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false);
} }
@ -153,3 +199,13 @@ describe("Storybook inventory", () => {
expect(legacyStories).toEqual([]); 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;
}