-
- {dashboard.eyebrow ?
{dashboard.eyebrow}
: null}
-
{dashboard.title}
- {dashboard.subtitle ?
{dashboard.subtitle} : null}
-
- {actions ? (
-
{actions}
- ) : null}
+
+ {dashboard.eyebrow ?
{dashboard.eyebrow}
: null}
+
{dashboard.title}
+ {dashboard.subtitle ?
{dashboard.subtitle} : null}
{dashboard.modules.length ? (
diff --git a/src/lib/ui/components/ThemeToggle.tsx b/src/lib/ui/components/ThemeToggle.tsx
deleted file mode 100644
index 1c8fb1a..0000000
--- a/src/lib/ui/components/ThemeToggle.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-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);
- const label = `Switch to ${nextTheme} theme`;
-
- return (
-
- );
-}
diff --git a/src/lib/ui/components/styles.css b/src/lib/ui/components/styles.css
index 086c0c4..d8111f4 100644
--- a/src/lib/ui/components/styles.css
+++ b/src/lib/ui/components/styles.css
@@ -8,8 +8,8 @@
overflow: hidden;
padding: clamp(0.42rem, 0.65vw, 0.62rem);
background:
- linear-gradient(90deg, transparent 0 49%, var(--ui-color-frame-spine) 50%, transparent 51%),
- var(--ui-color-frame-wash);
+ linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%),
+ rgba(0, 0, 0, 0.18);
}
.dashboard-frame__header,
@@ -26,22 +26,6 @@
padding-bottom: 0.34rem;
}
-.dashboard-frame__title {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: var(--ui-space-3);
- align-items: start;
- min-width: 0;
-}
-
-.dashboard-frame__title-copy {
- min-width: 0;
-}
-
-.dashboard-frame__actions {
- justify-self: end;
-}
-
.dashboard-header {
grid-template-columns: minmax(0, 1fr) auto;
padding: var(--ui-space-3);
@@ -115,7 +99,7 @@
.panel,
.system-state {
border: var(--ui-border);
- background: var(--ui-color-surface-panel);
+ background: rgba(3, 4, 3, 0.86);
color: var(--ui-color-text);
}
@@ -123,11 +107,7 @@
display: grid;
min-height: 5.15rem;
gap: 0.25rem;
- background: linear-gradient(
- 180deg,
- var(--ui-color-card-gradient-start),
- var(--ui-color-card-gradient-end)
- );
+ background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
padding: 0.45rem 0.55rem 0.42rem;
}
@@ -232,7 +212,7 @@
.progress-meter {
height: 0.25rem;
border: var(--ui-border);
- background: var(--ui-color-surface-inset);
+ background: #030403;
}
.telemetry-card__bar span,
@@ -291,7 +271,7 @@
align-items: start;
min-width: 0;
min-height: 4.15rem;
- background: var(--ui-color-surface-module);
+ background: rgba(6, 8, 7, 0.82);
padding: 0.5rem;
}
@@ -317,7 +297,7 @@
.module-card[data-severity="ok"] .icon-glyph {
background: var(--ui-color-accent);
- color: var(--ui-color-accent-contrast);
+ color: #050605;
}
.module-card[data-severity="warning"] {
@@ -368,7 +348,7 @@
.service-row:where(a):hover {
border-bottom-color: var(--ui-color-accent);
- background: var(--ui-color-hover);
+ background: rgba(244, 244, 244, 0.035);
}
.service-row__main {
@@ -418,7 +398,7 @@
grid-template-columns: auto minmax(0, 1fr);
min-height: 1.9rem;
align-items: center;
- background: var(--ui-color-surface-footer);
+ background: rgba(2, 3, 2, 0.92);
color: inherit;
text-decoration: none;
}
@@ -446,7 +426,7 @@
.footer-cell[data-severity="ok"] strong {
background: var(--ui-color-accent);
- color: var(--ui-color-accent-contrast);
+ color: #060706;
}
.footer-cell[data-severity="warning"] strong {
@@ -559,48 +539,18 @@
width: 2.5rem;
height: 2.5rem;
place-items: center;
- background: var(--ui-color-surface-footer);
+ background: rgba(2, 3, 2, 0.92);
color: var(--ui-color-muted);
padding: 0;
}
-.theme-toggle {
- display: inline-grid;
- grid-auto-flow: column;
- gap: var(--ui-space-2);
- align-items: center;
- justify-content: center;
- min-width: 6.5rem;
- min-height: 2.15rem;
- border: var(--ui-border);
- background: var(--ui-color-surface-raised);
- color: var(--ui-color-text);
- cursor: pointer;
- font: inherit;
- font-size: 0.62rem;
- font-weight: 850;
- letter-spacing: 0;
- padding: 0 var(--ui-space-3);
- text-transform: uppercase;
-}
-
-.theme-toggle:hover {
- border-color: var(--ui-color-accent);
- color: var(--ui-color-accent);
-}
-
-.theme-toggle span {
- line-height: 1;
- white-space: nowrap;
-}
-
.icon-glyph {
display: inline-grid;
width: 1.55rem;
height: 1.55rem;
place-items: center;
border: var(--ui-border);
- background: var(--ui-color-surface-icon);
+ background: #050605;
color: currentColor;
line-height: 1;
}
@@ -667,7 +617,6 @@
}
.dashboard-frame__header,
- .dashboard-frame__title,
.dashboard-header,
.dashboard-frame__modules,
.dashboard-frame__panels {
diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts
index 9eb5448..864306c 100644
--- a/src/lib/ui/index.ts
+++ b/src/lib/ui/index.ts
@@ -26,17 +26,9 @@ 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 { dashboardPreviewFixtures } from "./fixtures";
export { dashboardDocumentToUiDashboard } from "./model-renderer";
-export {
- getNextUiTheme,
- isUiTheme,
- persistUiTheme,
- resolveInitialUiTheme,
- UI_THEME_STORAGE_KEY,
-} from "./theme";
export type {
UiDashboardPreview,
UiLink,
@@ -48,4 +40,3 @@ export type {
UiStatusItem,
UiTelemetryCard,
} from "./types";
-export type { UiTheme } from "./theme";
diff --git a/src/lib/ui/stories/ThemeToggle.stories.tsx b/src/lib/ui/stories/ThemeToggle.stories.tsx
deleted file mode 100644
index 33e494a..0000000
--- a/src/lib/ui/stories/ThemeToggle.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ThemeToggle } from "../components/ThemeToggle";
-
-const meta = {
- title: "UI/ThemeToggle",
- component: ThemeToggle,
- args: {
- onThemeChange: () => undefined,
- theme: "dark",
- },
-} satisfies Meta;
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Dark: Story = {};
-
-export const Light: Story = {
- args: {
- theme: "light",
- },
- globals: {
- theme: "light",
- },
-};
diff --git a/src/lib/ui/storybook.test.ts b/src/lib/ui/storybook.test.ts
index aa76cad..f1303c7 100644
--- a/src/lib/ui/storybook.test.ts
+++ b/src/lib/ui/storybook.test.ts
@@ -36,7 +36,6 @@ const requiredStoryFiles = [
"TelemetryCard.stories.tsx",
"TelemetryGrid.stories.tsx",
"TelemetryStrip.stories.tsx",
- "ThemeToggle.stories.tsx",
"WeatherModule.stories.tsx",
] as const;
@@ -75,13 +74,6 @@ describe("Storybook inventory", () => {
expect(dashboardFrame).not.toContain('./styles.css');
});
- test("configures Storybook theme switching for reusable components", () => {
- const previewSource = readFileSync(join(root, ".storybook/preview.ts"), "utf8");
-
- expect(previewSource).toContain("globalTypes");
- expect(previewSource).toContain("data-ui-theme");
- });
-
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"))
diff --git a/src/lib/ui/theme.test.ts b/src/lib/ui/theme.test.ts
deleted file mode 100644
index 8ed3f04..0000000
--- a/src/lib/ui/theme.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { describe, expect, test } from "vitest";
-import {
- getNextUiTheme,
- isUiTheme,
- persistUiTheme,
- resolveInitialUiTheme,
- UI_THEME_STORAGE_KEY,
-} from "./theme";
-
-describe("UI theme preference", () => {
- test("defaults to dark when no stored preference exists", () => {
- const storage = new Map();
-
- expect(resolveInitialUiTheme(storage)).toBe("dark");
- });
-
- test("restores a valid stored preference", () => {
- const storage = new Map([[UI_THEME_STORAGE_KEY, "light"]]);
-
- expect(resolveInitialUiTheme(storage)).toBe("light");
- });
-
- test("ignores invalid stored preferences", () => {
- const storage = new Map([[UI_THEME_STORAGE_KEY, "solarized"]]);
-
- expect(resolveInitialUiTheme(storage)).toBe("dark");
- });
-
- test("falls back when stored preferences cannot be read", () => {
- const storage = {
- getItem() {
- throw new Error("storage blocked");
- },
- };
-
- expect(resolveInitialUiTheme(storage)).toBe("dark");
- });
-
- test("ignores persistence failures", () => {
- const storage = {
- setItem() {
- throw new Error("quota exceeded");
- },
- };
-
- expect(() => persistUiTheme("light", storage)).not.toThrow();
- });
-
- test("detects and toggles supported themes", () => {
- expect(isUiTheme("light")).toBe(true);
- expect(isUiTheme("dark")).toBe(true);
- expect(isUiTheme("contrast")).toBe(false);
- expect(getNextUiTheme("dark")).toBe("light");
- expect(getNextUiTheme("light")).toBe("dark");
- });
-});
diff --git a/src/lib/ui/theme.ts b/src/lib/ui/theme.ts
deleted file mode 100644
index 9f48df5..0000000
--- a/src/lib/ui/theme.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-export const UI_THEME_STORAGE_KEY = "dashboard-ui-theme";
-
-export type UiTheme = "dark" | "light";
-
-type ReadableThemeStorage =
- | { getItem(key: string): string | null }
- | { get(key: string): string | undefined };
-
-type WritableThemeStorage =
- | { setItem(key: string, value: string): void }
- | { set(key: string, value: string): unknown };
-
-export function isUiTheme(value: unknown): value is UiTheme {
- return value === "dark" || value === "light";
-}
-
-export function getNextUiTheme(theme: UiTheme): UiTheme {
- return theme === "dark" ? "light" : "dark";
-}
-
-export function resolveInitialUiTheme(
- storage?: ReadableThemeStorage | null,
- fallback: UiTheme = "dark",
-): UiTheme {
- const stored = safeReadStoredTheme(storage);
-
- return isUiTheme(stored) ? stored : fallback;
-}
-
-export function persistUiTheme(
- theme: UiTheme,
- storage?: WritableThemeStorage | null,
-): void {
- if (!storage) return;
-
- try {
- if ("setItem" in storage) {
- storage.setItem(UI_THEME_STORAGE_KEY, theme);
- return;
- }
-
- storage.set(UI_THEME_STORAGE_KEY, theme);
- } catch {
- // Browser storage may be blocked or quota-constrained.
- }
-}
-
-function safeReadStoredTheme(
- storage?: ReadableThemeStorage | null,
-): string | undefined {
- try {
- return readStoredTheme(storage);
- } catch {
- return undefined;
- }
-}
-
-function readStoredTheme(
- storage?: ReadableThemeStorage | null,
-): string | undefined {
- if (!storage) return undefined;
-
- if ("getItem" in storage) {
- return storage.getItem(UI_THEME_STORAGE_KEY) ?? undefined;
- }
-
- return storage.get(UI_THEME_STORAGE_KEY);
-}
diff --git a/src/lib/ui/tokens.css b/src/lib/ui/tokens.css
index 3e70132..56c2f7e 100644
--- a/src/lib/ui/tokens.css
+++ b/src/lib/ui/tokens.css
@@ -1,27 +1,14 @@
-:root,
-[data-ui-theme="dark"] {
+:root {
color-scheme: dark;
--ui-color-canvas: #020302;
--ui-color-surface: #060706;
--ui-color-surface-raised: #0b0d0c;
- --ui-color-surface-panel: rgba(3, 4, 3, 0.86);
- --ui-color-surface-module: rgba(6, 8, 7, 0.82);
- --ui-color-surface-footer: rgba(2, 3, 2, 0.92);
- --ui-color-surface-inset: #030403;
- --ui-color-surface-icon: #050605;
--ui-color-line: rgba(244, 244, 244, 0.16);
--ui-color-line-strong: rgba(244, 244, 244, 0.32);
- --ui-color-grid-line: rgba(255, 255, 255, 0.035);
- --ui-color-frame-spine: rgba(255, 255, 255, 0.08);
- --ui-color-frame-wash: rgba(0, 0, 0, 0.18);
- --ui-color-card-gradient-start: rgba(13, 15, 14, 0.82);
- --ui-color-card-gradient-end: rgba(2, 3, 2, 0.92);
- --ui-color-hover: rgba(244, 244, 244, 0.035);
--ui-color-text: #f3f4ed;
--ui-color-muted: #8d948c;
--ui-color-dim: #555b55;
--ui-color-accent: #d7ff00;
- --ui-color-accent-contrast: #050605;
--ui-color-ok: #bfff00;
--ui-color-warning: #ffb020;
--ui-color-danger: #ff1744;
@@ -46,37 +33,6 @@
--ui-focus-ring: 0 0 0 2px var(--ui-color-canvas), 0 0 0 4px var(--ui-color-accent);
}
-[data-ui-theme="light"] {
- color-scheme: light;
- --ui-color-canvas: #f3f5ed;
- --ui-color-surface: #ffffff;
- --ui-color-surface-raised: #eef1e7;
- --ui-color-surface-panel: rgba(255, 255, 255, 0.9);
- --ui-color-surface-module: rgba(255, 255, 255, 0.84);
- --ui-color-surface-footer: rgba(248, 250, 241, 0.94);
- --ui-color-surface-inset: #e4e8db;
- --ui-color-surface-icon: #f8faf1;
- --ui-color-line: rgba(19, 25, 19, 0.18);
- --ui-color-line-strong: rgba(19, 25, 19, 0.34);
- --ui-color-grid-line: rgba(19, 25, 19, 0.055);
- --ui-color-frame-spine: rgba(19, 25, 19, 0.1);
- --ui-color-frame-wash: rgba(255, 255, 255, 0.48);
- --ui-color-card-gradient-start: rgba(255, 255, 255, 0.92);
- --ui-color-card-gradient-end: rgba(230, 235, 221, 0.88);
- --ui-color-hover: rgba(19, 25, 19, 0.055);
- --ui-color-text: #11170f;
- --ui-color-muted: #5d665a;
- --ui-color-dim: #7d8779;
- --ui-color-accent: #516f00;
- --ui-color-accent-contrast: #ffffff;
- --ui-color-ok: #4c7400;
- --ui-color-warning: #a45b00;
- --ui-color-danger: #bd173f;
- --ui-color-stale: #596975;
- --ui-color-unavailable: #6c7479;
- --ui-shadow-hard: 0 0 0 1px rgba(81, 111, 0, 0.18) inset;
-}
-
html {
background: var(--ui-color-canvas);
}
@@ -86,8 +42,8 @@ body {
min-height: 100vh;
margin: 0;
background:
- linear-gradient(var(--ui-color-grid-line) 1px, transparent 1px),
- linear-gradient(90deg, var(--ui-color-grid-line) 1px, transparent 1px),
+ linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
var(--ui-color-canvas);
background-size: 48px 48px, 48px 48px, auto;
color: var(--ui-color-text);
diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts
index 7110900..b432d14 100644
--- a/tests/e2e/dashboard.spec.ts
+++ b/tests/e2e/dashboard.spec.ts
@@ -209,16 +209,6 @@ test.describe("dashboard page QA gate", () => {
await waitForDashboardReady(page);
await expect(page.getByRole("main")).toHaveCount(1);
- await page.keyboard.press("Tab");
- const themeToggle = page.getByRole("button", {
- name: "Switch to light theme",
- });
- await expect(themeToggle).toBeFocused();
- const themeFocusBoxShadow = await themeToggle.evaluate((element) => {
- return window.getComputedStyle(element).boxShadow;
- });
- expect(themeFocusBoxShadow).not.toBe("none");
-
for (const serviceId of linkedServiceIds) {
await page.keyboard.press("Tab");
@@ -240,24 +230,6 @@ test.describe("dashboard page QA gate", () => {
expect(results.violations).toEqual([]);
});
- test("passes automated accessibility checks in light mode", async ({
- page,
- }, testInfo) => {
- test.skip(testInfo.project.name !== "chromium-desktop");
-
- await page.goto("/");
- await waitForDashboardReady(page);
- await page.getByRole("button", { name: "Switch to light theme" }).click();
- await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
-
- const results = await new AxeBuilder({ page }).analyze();
- expect(results.violations).toEqual([]);
-
- await expect(page).toHaveScreenshot("dashboard-light-desktop.png", {
- fullPage: true,
- });
- });
-
test("honors reduced-motion preferences", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
@@ -283,29 +255,6 @@ test.describe("dashboard page QA gate", () => {
0.01,
);
});
-
- test("toggles the dashboard between dark and light themes", async ({ page }) => {
- await page.goto("/");
- await waitForDashboardReady(page);
-
- await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "dark");
-
- const switchToLight = page.getByRole("button", {
- name: "Switch to light theme",
- });
- await expect(switchToLight).toBeVisible();
- await switchToLight.click();
-
- await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
- await expect(
- page.getByRole("button", { name: "Switch to dark theme" }),
- ).toBeVisible();
-
- await page.reload();
- await waitForDashboardReady(page);
-
- await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
- });
});
async function waitForDashboardReady(page: Page): Promise {
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png
index 848033e..fa3b3bf 100644
Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png differ
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png
deleted file mode 100644
index 5b04c30..0000000
Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png and /dev/null differ
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png
index 1f6baf4..a199b15 100644
Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png differ
diff --git a/tests/e2e/storybook-server.ts b/tests/e2e/storybook-server.ts
deleted file mode 100644
index e6e3f63..0000000
--- a/tests/e2e/storybook-server.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { existsSync } from "node:fs";
-import { resolve, sep } from "node:path";
-
-const root = resolve(process.cwd(), "storybook-static");
-const port = Number(process.env.STORYBOOK_STATIC_PORT || 6007);
-
-if (!existsSync(resolve(root, "iframe.html"))) {
- throw new Error("storybook-static is missing. Run bun run build-storybook first.");
-}
-
-Bun.serve({
- hostname: "127.0.0.1",
- port,
- async fetch(request) {
- const url = new URL(request.url);
- const pathname = decodeURIComponent(url.pathname);
- const relativePath = pathname === "/" ? "/index.html" : pathname;
- const filePath = resolve(root, `.${relativePath}`);
-
- if (!filePath.startsWith(`${root}${sep}`)) {
- return new Response("Forbidden", { status: 403 });
- }
-
- const file = Bun.file(filePath);
- if (!(await file.exists())) {
- return new Response("Not found", { status: 404 });
- }
-
- return new Response(file);
- },
-});
-
-console.log(`Storybook static listening on http://127.0.0.1:${port}`);
diff --git a/tests/e2e/storybook.spec.ts b/tests/e2e/storybook.spec.ts
deleted file mode 100644
index 7873a02..0000000
--- a/tests/e2e/storybook.spec.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { expect, test } from "@playwright/test";
-
-const storybookPort = Number(process.env.PLAYWRIGHT_STORYBOOK_PORT || 6007);
-const storybookURL = `http://127.0.0.1:${storybookPort}`;
-
-test.describe("Storybook theme QA", () => {
- test("renders the ThemeToggle light story on the light canvas", async ({
- page,
- }, testInfo) => {
- test.skip(testInfo.project.name !== "chromium-desktop");
-
- const consoleMessages: string[] = [];
- page.on("console", (message) => {
- if (["error", "warning"].includes(message.type())) {
- consoleMessages.push(message.text());
- }
- });
-
- const url = `${storybookURL}/iframe.html?id=ui-themetoggle--light&viewMode=story`;
- await page.goto(url, { waitUntil: "networkidle" });
-
- await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
- await expect(
- page.getByRole("button", { name: "Switch to dark theme" }),
- ).toBeVisible();
- await expect(page.locator("body")).toHaveCSS(
- "background-color",
- "rgb(243, 245, 237)",
- );
- expect(
- consoleMessages.filter((message) =>
- message.includes("Global args/argTypes can only be set globally"),
- ),
- ).toEqual([]);
- });
-});