diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 32d1c14..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "packages/ui"] - path = packages/ui - url = ssh://git@git.dimensionlab.net/vince/dimensionlab-ui.git - branch = main diff --git a/README.md b/README.md index 00d9708..fee9443 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,14 @@ validated dashboard model state inside the web app. persistence, Playwright e2e checks, and container build. - `packages/dashboard-model`: reusable dashboard schema, validation, and generic model fixtures shared by apps and tooling. -- `packages/ui`: Git submodule for the reusable dashboard React components, - design tokens, shadcn/radix primitives, generic fixtures, and Storybook. - Component source is grouped under `foundation`, `frames`, `operations`, and - `telemetry` domains. +- `packages/ui`: reusable dashboard React components, design tokens, + 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 ```sh -git submodule update --init --recursive bun install bun run dev ``` @@ -129,7 +127,6 @@ The production build emits Vite client assets under `apps/web/dist/` and a Bun server entry at `apps/web/build/index.js`. A minimal deployment flow is: ```sh -git submodule update --init --recursive bun install --frozen-lockfile bun run build cd apps/web diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index c319cc6..71353f4 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -1,8 +1,7 @@ import { renderToString } from "react-dom/server"; import { describe, expect, test } from "vitest"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; -import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; -import { AppStateView, dashboardHydrationTiles } from "./App"; +import { AppStateView } from "./App"; describe("React app dashboard state view", () => { test("renders loading dashboard state", () => { @@ -55,7 +54,7 @@ describe("React app dashboard state view", () => { }} hydratingItemIds={new Set([ "telemetry:service-uptime", - "service:core-services:identity", + "service:identity", "module:ambient", "status:runtime:status", ])} @@ -74,12 +73,4 @@ describe("React app dashboard state view", () => { expect(html).toContain('data-severity="loading" data-model-id="ambient"'); expect(html).toContain('data-severity="loading" data-model-id="runtime:status"'); }); - - test("hydrates every status cell from the Dimension Lab shell", () => { - expect(dashboardHydrationTiles(dimensionLabDashboardFixture)).toContainEqual({ - kind: "status", - stripId: "footer-status", - id: "auto-refresh", - }); - }); }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 206b74f..6a19bd5 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -21,7 +21,7 @@ import { type DashboardTileReference = | { kind: "telemetry"; id: string } - | { kind: "service"; groupId: string; id: string } + | { kind: "service"; id: string } | { kind: "module"; id: string } | { kind: "status"; stripId: string; id: string }; @@ -295,7 +295,7 @@ function markHydratingItems( serviceGroups: dashboard.serviceGroups.map((group) => ({ ...group, services: group.services.map((service) => - hydratingItemIds.has(`service:${group.id}:${service.id}`) + hydratingItemIds.has(`service:${service.id}`) ? { ...service, severity: "loading", @@ -325,20 +325,14 @@ function markHydratingItems( }; } -export function dashboardHydrationTiles( - document: DashboardDocument, -): DashboardTileReference[] { +function dashboardHydrationTiles(document: DashboardDocument): DashboardTileReference[] { const telemetry = document.telemetry .filter((card) => card.datasource?.type === "external") .map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id })); const services = document.serviceGroups.flatMap((group) => group.services .filter((service) => service.datasource?.type === "external") - .map((service): DashboardTileReference => ({ - kind: "service", - groupId: group.id, - id: service.id, - })), + .map((service): DashboardTileReference => ({ kind: "service", id: service.id })), ); const modules = (document.modules || []) .filter((module) => @@ -348,6 +342,7 @@ export function dashboardHydrationTiles( .map((module): DashboardTileReference => ({ kind: "module", id: module.id })); const status = document.statusStrips.flatMap((strip) => strip.items + .filter((item) => item.id !== "auto-refresh") .map((item): DashboardTileReference => ({ kind: "status", stripId: strip.id, @@ -359,17 +354,15 @@ export function dashboardHydrationTiles( } function dashboardTileKey(tile: DashboardTileReference): string { - if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`; - if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`; - return `${tile.kind}:${tile.id}`; + return tile.kind === "status" + ? `${tile.kind}:${tile.stripId}:${tile.id}` + : `${tile.kind}:${tile.id}`; } function dashboardTileUrl(tile: DashboardTileReference): string { const parts = tile.kind === "status" ? ["api", "dashboard", "tile", tile.kind, tile.stripId, tile.id] - : tile.kind === "service" - ? ["api", "dashboard", "tile", tile.kind, tile.groupId, tile.id] - : ["api", "dashboard", "tile", tile.kind, tile.id]; + : ["api", "dashboard", "tile", tile.kind, tile.id]; return `/${parts.map(encodeURIComponent).join("/")}`; } @@ -387,16 +380,13 @@ function applyDashboardTile( } if (response.tile.kind === "service") { - const tile = response.tile; return { ...document, serviceGroups: document.serviceGroups.map((group) => ({ ...group, - services: group.id === tile.groupId - ? group.services.map((service) => - service.id === tile.id ? response.item as ServiceEntry : service, - ) - : group.services, + services: group.services.map((service) => + service.id === response.tile.id ? response.item as ServiceEntry : service, + ), })), }; } diff --git a/apps/web/src/lib/server/datasources/index.ts b/apps/web/src/lib/server/datasources/index.ts index ee24bcd..ff0c675 100644 --- a/apps/web/src/lib/server/datasources/index.ts +++ b/apps/web/src/lib/server/datasources/index.ts @@ -12,7 +12,7 @@ import type { export type DashboardTileReference = | { kind: "telemetry"; id: string } - | { kind: "service"; groupId: string; id: string } + | { kind: "service"; id: string } | { kind: "module"; id: string } | { kind: "status"; stripId: string; id: string }; @@ -32,11 +32,6 @@ export type DashboardTileResolution = state: "not_found"; tile: DashboardTileReference; message: string; - } - | { - state: "disabled"; - tile: DashboardTileReference; - message: string; }; export interface DatasourceResolutionOptions { @@ -95,8 +90,8 @@ export async function resolveDashboardTile( if (tile.kind === "service") { const service = document.serviceGroups - .find((group) => group.id === tile.groupId) - ?.services.find((item) => item.id === tile.id); + .flatMap((group) => group.services) + .find((item) => item.id === tile.id); if (!service) return missingTile(tile); return { @@ -837,7 +832,7 @@ function missingTile(tile: DashboardTileReference): DashboardTileResolution { } function tileKey(tile: DashboardTileReference): string { - if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`; - if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`; - return `${tile.kind}:${tile.id}`; + return tile.kind === "status" + ? `${tile.kind}:${tile.stripId}:${tile.id}` + : `${tile.kind}:${tile.id}`; } diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts index 095ee48..ae68e78 100644 --- a/apps/web/src/server/routes/dashboard.test.ts +++ b/apps/web/src/server/routes/dashboard.test.ts @@ -113,65 +113,6 @@ describe("dashboard API route", () => { expect.objectContaining({ cache: "no-store" }), ); }); - - test("hydrates a service tile with its service group identity", async () => { - const fetch = vi.fn(async () => - jsonResponse({ - status: "UP", - ping: 42, - }), - ); - - const response = await loadDashboardTileResponse( - { kind: "service", groupId: "essentials", id: "vaultwarden" }, - { - fetch, - refreshSeedDocument: true, - seedIfEmpty: true, - }, - ); - - expect(response.state).toBe("ready"); - if (response.state !== "ready") throw new Error("expected ready tile"); - expect(response.tile).toEqual({ - kind: "service", - groupId: "essentials", - id: "vaultwarden", - }); - expect(response.item).toMatchObject({ - id: "vaultwarden", - severity: "ok", - detail: "42 ms", - }); - }); - - test("does not hydrate tile routes when live datasources are disabled", async () => { - const previous = process.env.DISABLE_LIVE_DATASOURCES; - process.env.DISABLE_LIVE_DATASOURCES = "1"; - const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue( - new Error("live datasource fetch should not run when disabled"), - ); - - try { - const response = await loadDashboardTileResponse( - { kind: "telemetry", id: "infra-ram" }, - { - refreshSeedDocument: true, - seedIfEmpty: true, - }, - ); - - expect(response.state).toBe("disabled"); - expect(fetch).not.toHaveBeenCalled(); - } finally { - fetch.mockRestore(); - if (previous === undefined) { - delete process.env.DISABLE_LIVE_DATASOURCES; - } else { - process.env.DISABLE_LIVE_DATASOURCES = previous; - } - } - }); }); function jsonResponse(payload: unknown): Response { diff --git a/apps/web/src/server/routes/dashboard.ts b/apps/web/src/server/routes/dashboard.ts index fd0196e..d4a6b72 100644 --- a/apps/web/src/server/routes/dashboard.ts +++ b/apps/web/src/server/routes/dashboard.ts @@ -63,17 +63,6 @@ export async function loadDashboardTileResponse( tile: DashboardTileReference, options: LoadDashboardResponseOptions = {}, ): Promise { - if ( - options.disableLiveDatasources || - process.env.DISABLE_LIVE_DATASOURCES === "1" - ) { - return { - state: "disabled", - tile, - message: "Live datasource hydration is disabled.", - }; - } - const dashboard = loadDashboardRuntime(undefined, { refreshSeedDocument: options.refreshSeedDocument ?? true, seedIfEmpty: options.seedIfEmpty ?? true, @@ -106,7 +95,7 @@ export async function handleDashboardTileRoute(pathname: string): Promise { + const theme = context.globals.theme === "light" ? "light" : "dark"; + + if (typeof document !== "undefined") { + document.documentElement.setAttribute("data-ui-theme", theme); + } + + return Story(); + }, + ], + globalTypes: { + theme: { + description: "Dashboard component theme", + defaultValue: "dark", + toolbar: { + title: "Theme", + icon: "circlehollow", + items: [ + { value: "dark", title: "Dark" }, + { value: "light", title: "Light" }, + ], + dynamicTitle: true, + }, + }, + }, + parameters: { + backgrounds: { + default: "canvas", + values: [ + { name: "canvas", value: "#0b0f0d" }, + { name: "raised", value: "#151d18" }, + { name: "light canvas", value: "#eef2e7" }, + { name: "light raised", value: "#f1f5ea" }, + ], + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: "fullscreen", + }, +}; + +export default preview; diff --git a/packages/ui/components.json b/packages/ui/components.json new file mode 100644 index 0000000..d951369 --- /dev/null +++ b/packages/ui/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "src/components", + "utils": "src/utils", + "ui": "src/primitives", + "lib": "src", + "hooks": "src/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..a30db52 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,79 @@ +{ + "name": "@dimensionlab/ui", + "version": "0.0.1", + "private": true, + "type": "module", + "sideEffects": ["*.css", "**/*.css"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "development": "./src/index.ts", + "default": "./dist/index.js" + }, + "./foundation": { + "types": "./dist/components/foundation/index.d.ts", + "development": "./src/components/foundation/index.ts", + "default": "./dist/components/foundation/index.js" + }, + "./frames": { + "types": "./dist/components/frames/index.d.ts", + "development": "./src/components/frames/index.ts", + "default": "./dist/components/frames/index.js" + }, + "./operations": { + "types": "./dist/components/operations/index.d.ts", + "development": "./src/components/operations/index.ts", + "default": "./dist/components/operations/index.js" + }, + "./telemetry": { + "types": "./dist/components/telemetry/index.d.ts", + "development": "./src/components/telemetry/index.ts", + "default": "./dist/components/telemetry/index.js" + }, + "./styles.css": { + "development": "./src/styles.css", + "default": "./dist/styles.css" + }, + "./tokens.css": { + "development": "./src/tokens.css", + "default": "./dist/tokens.css" + } + }, + "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/components && cp src/styles.css dist/styles.css && cp src/tokens.css dist/tokens.css && cp src/components/styles.css dist/components/styles.css", + "check": "tsc --noEmit", + "test": "vitest run", + "test:unit": "vitest run", + "storybook": "storybook dev -p 6006 --host 0.0.0.0", + "build-storybook": "storybook build" + }, + "dependencies": { + "@fontsource-variable/geist": "^5.2.9", + "@iconify/react": "^6.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.21.0", + "radix-ui": "^1.6.0", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "uplot": "^1.6.32" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@storybook/addon-a11y": "^10.4.6", + "@storybook/addon-vitest": "^10.4.6", + "@storybook/react-vite": "^10.4.6", + "@types/bun": "^1.3.14", + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "bun-types": "^1.3.14", + "storybook": "^10.4.6", + "typescript": "^6.0.3", + "vite": "^8.0.16", + "vitest": "^4.1.9" + } +} diff --git a/packages/ui/src/components/foundation/Badge.tsx b/packages/ui/src/components/foundation/Badge.tsx new file mode 100644 index 0000000..5bdee48 --- /dev/null +++ b/packages/ui/src/components/foundation/Badge.tsx @@ -0,0 +1,11 @@ +import type { UiSeverity } from "../../types"; +import { StatusBadge } from "./StatusBadge"; + +export interface BadgeProps { + label: string; + severity?: UiSeverity; +} + +export function Badge({ label, severity = "neutral" }: BadgeProps) { + return ; +} diff --git a/packages/ui/src/components/foundation/Button.tsx b/packages/ui/src/components/foundation/Button.tsx new file mode 100644 index 0000000..8ae3b4e --- /dev/null +++ b/packages/ui/src/components/foundation/Button.tsx @@ -0,0 +1,39 @@ +import type { ButtonHTMLAttributes } from "react"; +import { IconGlyph } from "./IconGlyph"; + +export interface ButtonProps + extends Omit, "type"> { + label: string; + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "default" | "compact"; + icon?: string; + loading?: boolean; + type?: "button" | "submit" | "reset"; +} + +export function Button({ + label, + variant = "primary", + size = "default", + icon, + disabled = false, + loading = false, + type = "button", + className = "", + ...buttonProps +}: ButtonProps) { + return ( + + ); +} diff --git a/packages/ui/src/components/foundation/IconButton.tsx b/packages/ui/src/components/foundation/IconButton.tsx new file mode 100644 index 0000000..92fcb46 --- /dev/null +++ b/packages/ui/src/components/foundation/IconButton.tsx @@ -0,0 +1,33 @@ +import type { ButtonHTMLAttributes } from "react"; +import { IconGlyph } from "./IconGlyph"; + +export interface IconButtonProps + extends Omit, "type"> { + icon: string; + label: string; + active?: boolean; + type?: "button" | "submit" | "reset"; +} + +export function IconButton({ + icon, + label, + active = false, + disabled = false, + type = "button", + className = "", + ...buttonProps +}: IconButtonProps) { + return ( + + ); +} diff --git a/packages/ui/src/components/foundation/IconGlyph.tsx b/packages/ui/src/components/foundation/IconGlyph.tsx new file mode 100644 index 0000000..d21feb1 --- /dev/null +++ b/packages/ui/src/components/foundation/IconGlyph.tsx @@ -0,0 +1,21 @@ +import { Icon } from "@iconify/react"; + +export interface IconGlyphProps { + name?: string; + label?: string; + size?: "sm" | "md" | "lg"; +} + +export function IconGlyph({ name, label, size = "md" }: IconGlyphProps) { + return ( + + {name ? : null} + + ); +} diff --git a/packages/ui/src/components/foundation/ProgressMeter.tsx b/packages/ui/src/components/foundation/ProgressMeter.tsx new file mode 100644 index 0000000..c3f3aad --- /dev/null +++ b/packages/ui/src/components/foundation/ProgressMeter.tsx @@ -0,0 +1,34 @@ +import type { CSSProperties } from "react"; +import { clampPercent } from "../../format"; +import type { UiSeverity } from "../../types"; + +export interface ProgressMeterProps { + value?: number; + severity?: UiSeverity; + label?: string; +} + +export function ProgressMeter({ + value, + severity = "neutral", + label = "Progress", +}: ProgressMeterProps) { + const progress = value === undefined ? null : clampPercent(value); + + return ( +
+ {progress !== null ? ( + + ) : null} +
+ ); +} diff --git a/packages/ui/src/components/foundation/Separator.tsx b/packages/ui/src/components/foundation/Separator.tsx new file mode 100644 index 0000000..ca1a955 --- /dev/null +++ b/packages/ui/src/components/foundation/Separator.tsx @@ -0,0 +1,19 @@ +export interface SeparatorProps { + orientation?: "horizontal" | "vertical"; + dense?: boolean; +} + +export function Separator({ + orientation = "horizontal", + dense = false, +}: SeparatorProps) { + return ( +
+ ); +} diff --git a/packages/ui/src/components/foundation/StatusBadge.tsx b/packages/ui/src/components/foundation/StatusBadge.tsx new file mode 100644 index 0000000..d2b01b3 --- /dev/null +++ b/packages/ui/src/components/foundation/StatusBadge.tsx @@ -0,0 +1,14 @@ +import type { UiSeverity } from "../../types"; + +export interface StatusBadgeProps { + label: string; + severity?: UiSeverity; +} + +export function StatusBadge({ label, severity = "neutral" }: StatusBadgeProps) { + return ( + + {label} + + ); +} diff --git a/packages/ui/src/components/foundation/ThemeToggle.tsx b/packages/ui/src/components/foundation/ThemeToggle.tsx new file mode 100644 index 0000000..5d83031 --- /dev/null +++ b/packages/ui/src/components/foundation/ThemeToggle.tsx @@ -0,0 +1,36 @@ +import type { UiTheme } from "../../theme"; +import { getNextUiTheme } from "../../theme"; +import { IconGlyph } from "./IconGlyph"; + +export interface ThemeToggleProps { + theme: UiTheme; + onThemeChange: (theme: UiTheme) => void; +} + +export function ThemeToggle({ theme, onThemeChange }: ThemeToggleProps) { + const nextTheme = getNextUiTheme(theme); + + return ( + + ); +} diff --git a/packages/ui/src/components/foundation/index.ts b/packages/ui/src/components/foundation/index.ts new file mode 100644 index 0000000..b82cd47 --- /dev/null +++ b/packages/ui/src/components/foundation/index.ts @@ -0,0 +1,8 @@ +export * from "./Badge"; +export * from "./Button"; +export * from "./IconButton"; +export * from "./IconGlyph"; +export * from "./ProgressMeter"; +export * from "./Separator"; +export * from "./StatusBadge"; +export * from "./ThemeToggle"; diff --git a/packages/ui/src/components/frames/CornerBracketFrame.tsx b/packages/ui/src/components/frames/CornerBracketFrame.tsx new file mode 100644 index 0000000..1dfd4f7 --- /dev/null +++ b/packages/ui/src/components/frames/CornerBracketFrame.tsx @@ -0,0 +1,26 @@ +export interface CornerBracketFrameProps { + density?: "regular" | "tight"; + size?: "sm" | "md" | "lg"; + tone?: "neutral" | "accent" | "danger"; +} + +export function CornerBracketFrame({ + density = "regular", + size = "md", + tone = "neutral", +}: CornerBracketFrameProps) { + return ( + + ); +} diff --git a/packages/ui/src/components/frames/DashboardFrame.tsx b/packages/ui/src/components/frames/DashboardFrame.tsx new file mode 100644 index 0000000..08029cf --- /dev/null +++ b/packages/ui/src/components/frames/DashboardFrame.tsx @@ -0,0 +1,59 @@ +import { useId } from "react"; +import type { ReactNode } from "react"; +import type { UiDashboardPreview } from "../../types"; +import { ModuleCard } from "./ModuleCard"; +import { ServicePanel } from "../operations/ServicePanel"; +import { StatusStrip } from "../operations/StatusStrip"; +import { TelemetryGrid } from "../telemetry/TelemetryGrid"; + +export interface DashboardFrameProps { + actions?: ReactNode; + dashboard: UiDashboardPreview; + titleId?: string; +} + +export function DashboardFrame({ + actions, + dashboard, + titleId, +}: DashboardFrameProps) { + const generatedTitleId = useId(); + const resolvedTitleId = titleId || `${generatedTitleId}-title`; + + return ( +
+
+
+
+ {dashboard.eyebrow ?

{dashboard.eyebrow}

: null} +

{dashboard.title}

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

{eyebrow}

: null} +

{title}

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