From addc9c7b1a7b6dfe4551db16741f0dc5470db783 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 25 Aug 2026 20:05:56 +0200 Subject: [PATCH] feat: build housekeeping command deck shell --- .../page/housekeeping-page-shell.tsx | 35 ++++ .../page/housekeeping-page-state.test.tsx | 84 +++++++++ .../page/housekeeping-page-state.tsx | 47 +++++ .../foundation/shell/command-trigger.tsx | 15 ++ .../foundation/shell/context-nav.tsx | 25 +++ .../foundation/shell/domain-rail.tsx | 49 +++++ .../shell/housekeeping-shell.test.tsx | 174 ++++++++++++++++++ .../foundation/shell/housekeeping-shell.tsx | 80 ++++++++ .../foundation/shell/operator-summary.tsx | 19 ++ 9 files changed, 528 insertions(+) create mode 100644 src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx create mode 100644 src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx create mode 100644 src/features/housekeeping/foundation/page/housekeeping-page-state.tsx create mode 100644 src/features/housekeeping/foundation/shell/command-trigger.tsx create mode 100644 src/features/housekeeping/foundation/shell/context-nav.tsx create mode 100644 src/features/housekeeping/foundation/shell/domain-rail.tsx create mode 100644 src/features/housekeeping/foundation/shell/housekeeping-shell.test.tsx create mode 100644 src/features/housekeeping/foundation/shell/housekeeping-shell.tsx create mode 100644 src/features/housekeeping/foundation/shell/operator-summary.tsx diff --git a/src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx b/src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx new file mode 100644 index 00000000..07a48921 --- /dev/null +++ b/src/features/housekeeping/foundation/page/housekeeping-page-shell.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from "react"; + +interface HousekeepingPageShellProps { + title: string; + description: string; + primaryAction?: ReactNode; + context?: ReactNode; + children: ReactNode; +} + +export function HousekeepingPageShell({ + title, + description, + primaryAction, + context, + children, +}: HousekeepingPageShellProps) { + return ( +
+
+
+

+ {title} +

+

+ {description} +

+ {context ?
{context}
: null} +
+ {primaryAction ?
{primaryAction}
: null} +
+
{children}
+
+ ); +} diff --git a/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx b/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx new file mode 100644 index 00000000..b835b231 --- /dev/null +++ b/src/features/housekeeping/foundation/page/housekeeping-page-state.test.tsx @@ -0,0 +1,84 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { HousekeepingPageShell } from "./housekeeping-page-shell"; +import { HousekeepingPageState } from "./housekeeping-page-state"; + +describe("HousekeepingPageState", () => { + it.each([ + ["loading", "status"], + ["empty", "status"], + ["partial", "status"], + ["error", "alert"], + ] as const)("renders %s with the %s role", (state, role) => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain(`role="${role}"`); + expect(html).toContain(`>${state} title<`); + expect(html).toContain(`>${state} description<`); + }); + + it("announces loading politely and makes the partial warning visible", () => { + const loading = renderToStaticMarkup( + , + ); + const partial = renderToStaticMarkup( + , + ); + + expect(loading).toContain('aria-live="polite"'); + expect(partial).toContain('data-state-tone="warning"'); + expect(partial).toContain(">Partial title<"); + }); + + it("renders supplied retry content for an error without owning a callback", () => { + const html = renderToStaticMarkup( + Retry preview} + />, + ); + + expect(html).toContain('role="alert"'); + expect(html).toContain('href="/admin-next/operations"'); + expect(html).toContain(">Retry preview<"); + }); +}); + +describe("HousekeepingPageShell", () => { + it("renders supplied header, context, action, and body content", () => { + const html = renderToStaticMarkup( + Preview context} + primaryAction={Create preview} + > +

Page body

+
, + ); + + expect(html).toContain("People<"); + expect(html).toContain(">Review operator-facing people data<"); + expect(html).toContain(">Preview context<"); + expect(html).toContain('href="/admin-next/people/new"'); + expect(html).toContain(">Create preview<"); + expect(html).toContain(">Page body

"); + }); +}); diff --git a/src/features/housekeeping/foundation/page/housekeeping-page-state.tsx b/src/features/housekeeping/foundation/page/housekeeping-page-state.tsx new file mode 100644 index 00000000..dc2e2a56 --- /dev/null +++ b/src/features/housekeeping/foundation/page/housekeeping-page-state.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from "react"; + +interface HousekeepingPageStateProps { + state: "loading" | "empty" | "partial" | "error"; + title: string; + description: string; + retryAction?: ReactNode; +} + +export function HousekeepingPageState({ + state, + title, + description, + retryAction, +}: HousekeepingPageStateProps) { + const isError = state === "error"; + const isPartial = state === "partial"; + + return ( +
+ {isPartial ? ( +

+ {title} +

+ ) : ( +

{title}

+ )} +

+ {description} +

+ {retryAction ?
{retryAction}
: null} +
+ ); +} diff --git a/src/features/housekeeping/foundation/shell/command-trigger.tsx b/src/features/housekeeping/foundation/shell/command-trigger.tsx new file mode 100644 index 00000000..80065ae6 --- /dev/null +++ b/src/features/housekeeping/foundation/shell/command-trigger.tsx @@ -0,0 +1,15 @@ +interface CommandTriggerProps { + label: string; +} + +export function CommandTrigger({ label }: CommandTriggerProps) { + return ( + + ); +} diff --git a/src/features/housekeeping/foundation/shell/context-nav.tsx b/src/features/housekeeping/foundation/shell/context-nav.tsx new file mode 100644 index 00000000..46926fb0 --- /dev/null +++ b/src/features/housekeeping/foundation/shell/context-nav.tsx @@ -0,0 +1,25 @@ +import Link from "next/link"; + +interface ContextNavProps { + items: readonly { id: string; href: string; label: string }[]; + ariaLabel: string; +} + +export function ContextNav({ items, ariaLabel }: ContextNavProps) { + return ( + + ); +} diff --git a/src/features/housekeeping/foundation/shell/domain-rail.tsx b/src/features/housekeeping/foundation/shell/domain-rail.tsx new file mode 100644 index 00000000..9bcdc5a6 --- /dev/null +++ b/src/features/housekeeping/foundation/shell/domain-rail.tsx @@ -0,0 +1,49 @@ +import { FileText, Gem, Hotel, Inbox, Settings, Users } from "lucide-react"; +import Link from "next/link"; +import type { HousekeepingNavigationDomain } from "../navigation"; + +interface DomainRailProps { + domains: readonly HousekeepingNavigationDomain[]; + activeDomainId: HousekeepingNavigationDomain["id"]; + ariaLabel: string; +} + +function DomainIcon({ iconId }: Pick) { + const Icon = { + inbox: Inbox, + users: Users, + "file-text": FileText, + gem: Gem, + hotel: Hotel, + settings: Settings, + }[iconId]; + + return