Compare commits
No commits in common. "c84de9aaa9f5c538e6335f27cbc692b8146cd195" and "bbc81cea0db36e5780cfa392fd0f6d9962a7d77a" have entirely different histories.
c84de9aaa9
...
bbc81cea0d
21 changed files with 30 additions and 564 deletions
|
|
@ -14,40 +14,12 @@ if (typeof window !== "undefined") {
|
||||||
}
|
}
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
decorators: [
|
|
||||||
(Story, context) => {
|
|
||||||
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: {
|
parameters: {
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
default: "canvas",
|
default: "canvas",
|
||||||
values: [
|
values: [
|
||||||
{ name: "canvas", value: "#020302" },
|
{ name: "canvas", value: "#020302" },
|
||||||
{ name: "raised", value: "#0b0d0c" },
|
{ name: "raised", value: "#0b0d0c" },
|
||||||
{ name: "light canvas", value: "#f3f5ed" },
|
|
||||||
{ name: "light raised", value: "#eef1e7" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
controls: {
|
controls: {
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,6 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Dimension Lab dashboard runtime" />
|
<meta name="description" content="Dimension Lab dashboard runtime" />
|
||||||
<title>Dimension Lab</title>
|
<title>Dimension Lab</title>
|
||||||
<script>
|
|
||||||
try {
|
|
||||||
const theme = localStorage.getItem("dashboard-ui-theme");
|
|
||||||
document.documentElement.dataset.uiTheme =
|
|
||||||
theme === "light" ? "light" : "dark";
|
|
||||||
} catch {
|
|
||||||
document.documentElement.dataset.uiTheme = "dark";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
const port = Number(process.env.PLAYWRIGHT_PORT || 4173);
|
const port = Number(process.env.PLAYWRIGHT_PORT || 4173);
|
||||||
const baseURL = `http://127.0.0.1:${port}`;
|
const baseURL = `http://127.0.0.1:${port}`;
|
||||||
const storybookPort = Number(process.env.PLAYWRIGHT_STORYBOOK_PORT || 6007);
|
|
||||||
const storybookURL = `http://127.0.0.1:${storybookPort}`;
|
|
||||||
const databaseUrl =
|
const databaseUrl =
|
||||||
process.env.PLAYWRIGHT_DATABASE_URL ||
|
process.env.PLAYWRIGHT_DATABASE_URL ||
|
||||||
`file:./data/playwright-${process.pid}-${Date.now()}.sqlite`;
|
`file:./data/playwright-${process.pid}-${Date.now()}.sqlite`;
|
||||||
|
|
@ -19,20 +17,12 @@ export default defineConfig({
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
screenshot: "only-on-failure",
|
screenshot: "only-on-failure",
|
||||||
},
|
},
|
||||||
webServer: [
|
webServer: {
|
||||||
{
|
command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`,
|
||||||
command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`,
|
url: baseURL,
|
||||||
url: baseURL,
|
reuseExistingServer: false,
|
||||||
reuseExistingServer: false,
|
timeout: 120_000,
|
||||||
timeout: 120_000,
|
},
|
||||||
},
|
|
||||||
{
|
|
||||||
command: `bun run build-storybook && STORYBOOK_STATIC_PORT=${storybookPort} bun tests/e2e/storybook-server.ts`,
|
|
||||||
url: storybookURL,
|
|
||||||
reuseExistingServer: false,
|
|
||||||
timeout: 120_000,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium-desktop",
|
name: "chromium-desktop",
|
||||||
|
|
|
||||||
|
|
@ -18,24 +18,4 @@ describe("React app dashboard state view", () => {
|
||||||
expect(html).toContain("Loading Dashboard");
|
expect(html).toContain("Loading Dashboard");
|
||||||
expect(html).toContain("Fetching active model");
|
expect(html).toContain("Fetching active model");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders an accessible theme toggle with the active theme", () => {
|
|
||||||
const html = renderToString(
|
|
||||||
<AppStateView
|
|
||||||
dashboard={{
|
|
||||||
state: "loading",
|
|
||||||
title: "Loading Dashboard",
|
|
||||||
subtitle: "Fetching active model",
|
|
||||||
message: "Waiting for the active dashboard document.",
|
|
||||||
}}
|
|
||||||
theme="dark"
|
|
||||||
onThemeChange={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(html).toContain('data-ui-theme-toggle="true"');
|
|
||||||
expect(html).toContain('aria-label="Switch to light theme"');
|
|
||||||
expect(html).not.toContain("aria-pressed");
|
|
||||||
expect(html).toContain("Dark");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
43
src/App.tsx
43
src/App.tsx
|
|
@ -3,11 +3,7 @@ import type { DashboardRuntimeState } from "$lib/server/dashboard";
|
||||||
import {
|
import {
|
||||||
DashboardFrame,
|
DashboardFrame,
|
||||||
SystemState,
|
SystemState,
|
||||||
ThemeToggle,
|
|
||||||
dashboardDocumentToUiDashboard,
|
dashboardDocumentToUiDashboard,
|
||||||
persistUiTheme,
|
|
||||||
resolveInitialUiTheme,
|
|
||||||
type UiTheme,
|
|
||||||
type UiSeverity,
|
type UiSeverity,
|
||||||
} from "$lib/ui";
|
} from "$lib/ui";
|
||||||
|
|
||||||
|
|
@ -20,23 +16,13 @@ const loadingDashboardState: DashboardRuntimeState = {
|
||||||
|
|
||||||
export function AppStateView({
|
export function AppStateView({
|
||||||
dashboard,
|
dashboard,
|
||||||
onThemeChange,
|
|
||||||
theme,
|
|
||||||
}: {
|
}: {
|
||||||
dashboard: DashboardRuntimeState;
|
dashboard: DashboardRuntimeState;
|
||||||
onThemeChange?: (theme: UiTheme) => void;
|
|
||||||
theme?: UiTheme;
|
|
||||||
}) {
|
}) {
|
||||||
const themeToggle =
|
|
||||||
theme && onThemeChange ? (
|
|
||||||
<ThemeToggle theme={theme} onThemeChange={onThemeChange} />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
if (dashboard.state === "ready") {
|
if (dashboard.state === "ready") {
|
||||||
return (
|
return (
|
||||||
<DashboardFrame
|
<DashboardFrame
|
||||||
dashboard={dashboardDocumentToUiDashboard(dashboard.document)}
|
dashboard={dashboardDocumentToUiDashboard(dashboard.document)}
|
||||||
actions={themeToggle}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -46,9 +32,6 @@ export function AppStateView({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="state-shell" data-dashboard-state={dashboard.state}>
|
<main className="state-shell" data-dashboard-state={dashboard.state}>
|
||||||
{themeToggle ? (
|
|
||||||
<div className="state-shell__actions">{themeToggle}</div>
|
|
||||||
) : null}
|
|
||||||
<SystemState
|
<SystemState
|
||||||
title={dashboard.title}
|
title={dashboard.title}
|
||||||
detail={detail}
|
detail={detail}
|
||||||
|
|
@ -90,11 +73,6 @@ export function resolveDocumentMetadata(dashboard: DashboardRuntimeState): {
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [dashboard, setDashboard] =
|
const [dashboard, setDashboard] =
|
||||||
useState<DashboardRuntimeState>(loadingDashboardState);
|
useState<DashboardRuntimeState>(loadingDashboardState);
|
||||||
const [theme, setTheme] = useState<UiTheme>(() => {
|
|
||||||
if (typeof window === "undefined") return "dark";
|
|
||||||
|
|
||||||
return resolveInitialUiTheme(getThemeStorage());
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const metadata = resolveDocumentMetadata(dashboard);
|
const metadata = resolveDocumentMetadata(dashboard);
|
||||||
|
|
@ -111,11 +89,6 @@ export default function App() {
|
||||||
description.content = metadata.description;
|
description.content = metadata.description;
|
||||||
}, [dashboard]);
|
}, [dashboard]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.dataset.uiTheme = theme;
|
|
||||||
persistUiTheme(theme, getThemeStorage());
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let refreshTimer: number | undefined;
|
let refreshTimer: number | undefined;
|
||||||
|
|
@ -153,13 +126,7 @@ export default function App() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return <AppStateView dashboard={dashboard} />;
|
||||||
<AppStateView
|
|
||||||
dashboard={dashboard}
|
|
||||||
theme={theme}
|
|
||||||
onThemeChange={setTheme}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
|
function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
|
||||||
|
|
@ -173,11 +140,3 @@ function stateIcon(state: DashboardRuntimeState["state"]): string {
|
||||||
if (state === "loading") return "mdi:progress-clock";
|
if (state === "loading") return "mdi:progress-clock";
|
||||||
return "mdi:tray";
|
return "mdi:tray";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThemeStorage(): Storage | undefined {
|
|
||||||
try {
|
|
||||||
return window.localStorage;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
10
src/app.css
10
src/app.css
|
|
@ -107,8 +107,8 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background:
|
background:
|
||||||
linear-gradient(var(--ui-color-grid-line) 1px, transparent 1px),
|
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
linear-gradient(90deg, var(--ui-color-grid-line) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
var(--ui-color-canvas);
|
var(--ui-color-canvas);
|
||||||
background-size: 48px 48px, 48px 48px, auto;
|
background-size: 48px 48px, 48px 48px, auto;
|
||||||
color: var(--ui-color-text);
|
color: var(--ui-color-text);
|
||||||
|
|
@ -142,17 +142,13 @@ a {
|
||||||
padding: var(--ui-space-4);
|
padding: var(--ui-space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.state-shell__actions {
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.state-shell ul {
|
.state-shell ul {
|
||||||
display: grid;
|
display: grid;
|
||||||
max-width: 56rem;
|
max-width: 56rem;
|
||||||
gap: var(--ui-space-2);
|
gap: var(--ui-space-2);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: var(--ui-border);
|
border: var(--ui-border);
|
||||||
background: var(--ui-color-surface-module);
|
background: rgba(6, 8, 7, 0.82);
|
||||||
color: var(--ui-color-muted);
|
color: var(--ui-color-muted);
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
list-style-position: inside;
|
list-style-position: inside;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useId } from "react";
|
import { useId } 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 "./ServicePanel";
|
||||||
|
|
@ -7,31 +6,21 @@ import { StatusStrip } from "./StatusStrip";
|
||||||
import { TelemetryGrid } from "./TelemetryGrid";
|
import { TelemetryGrid } from "./TelemetryGrid";
|
||||||
|
|
||||||
export interface DashboardFrameProps {
|
export interface DashboardFrameProps {
|
||||||
actions?: ReactNode;
|
|
||||||
dashboard: UiDashboardPreview;
|
dashboard: UiDashboardPreview;
|
||||||
titleId?: string;
|
titleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardFrame({
|
export function DashboardFrame({ dashboard, titleId }: DashboardFrameProps) {
|
||||||
actions,
|
|
||||||
dashboard,
|
|
||||||
titleId,
|
|
||||||
}: DashboardFrameProps) {
|
|
||||||
const generatedTitleId = useId();
|
const generatedTitleId = useId();
|
||||||
const resolvedTitleId = titleId || `${generatedTitleId}-title`;
|
const resolvedTitleId = titleId || `${generatedTitleId}-title`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="dashboard-frame" aria-labelledby={resolvedTitleId}>
|
<main className="dashboard-frame" aria-labelledby={resolvedTitleId}>
|
||||||
<header className="dashboard-frame__header">
|
<header className="dashboard-frame__header">
|
||||||
<div className="dashboard-frame__title">
|
<div>
|
||||||
<div className="dashboard-frame__title-copy">
|
{dashboard.eyebrow ? <p>{dashboard.eyebrow}</p> : null}
|
||||||
{dashboard.eyebrow ? <p>{dashboard.eyebrow}</p> : null}
|
<h1 id={resolvedTitleId}>{dashboard.title}</h1>
|
||||||
<h1 id={resolvedTitleId}>{dashboard.title}</h1>
|
{dashboard.subtitle ? <span>{dashboard.subtitle}</span> : null}
|
||||||
{dashboard.subtitle ? <span>{dashboard.subtitle}</span> : null}
|
|
||||||
</div>
|
|
||||||
{actions ? (
|
|
||||||
<div className="dashboard-frame__actions">{actions}</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
{dashboard.modules.length ? (
|
{dashboard.modules.length ? (
|
||||||
<div className="dashboard-frame__modules">
|
<div className="dashboard-frame__modules">
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<button
|
|
||||||
aria-label={label}
|
|
||||||
className="theme-toggle"
|
|
||||||
data-ui-theme-toggle="true"
|
|
||||||
onClick={() => onThemeChange(nextTheme)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<IconGlyph
|
|
||||||
name={theme === "dark" ? "mdi:weather-night" : "mdi:white-balance-sunny"}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<span>{theme === "dark" ? "Dark" : "Light"}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: clamp(0.42rem, 0.65vw, 0.62rem);
|
padding: clamp(0.42rem, 0.65vw, 0.62rem);
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, transparent 0 49%, var(--ui-color-frame-spine) 50%, transparent 51%),
|
linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%),
|
||||||
var(--ui-color-frame-wash);
|
rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-frame__header,
|
.dashboard-frame__header,
|
||||||
|
|
@ -26,22 +26,6 @@
|
||||||
padding-bottom: 0.34rem;
|
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 {
|
.dashboard-header {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
padding: var(--ui-space-3);
|
padding: var(--ui-space-3);
|
||||||
|
|
@ -115,7 +99,7 @@
|
||||||
.panel,
|
.panel,
|
||||||
.system-state {
|
.system-state {
|
||||||
border: var(--ui-border);
|
border: var(--ui-border);
|
||||||
background: var(--ui-color-surface-panel);
|
background: rgba(3, 4, 3, 0.86);
|
||||||
color: var(--ui-color-text);
|
color: var(--ui-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,11 +107,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 5.15rem;
|
min-height: 5.15rem;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
background: linear-gradient(
|
background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
|
||||||
180deg,
|
|
||||||
var(--ui-color-card-gradient-start),
|
|
||||||
var(--ui-color-card-gradient-end)
|
|
||||||
);
|
|
||||||
padding: 0.45rem 0.55rem 0.42rem;
|
padding: 0.45rem 0.55rem 0.42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +212,7 @@
|
||||||
.progress-meter {
|
.progress-meter {
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
border: var(--ui-border);
|
border: var(--ui-border);
|
||||||
background: var(--ui-color-surface-inset);
|
background: #030403;
|
||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-card__bar span,
|
.telemetry-card__bar span,
|
||||||
|
|
@ -291,7 +271,7 @@
|
||||||
align-items: start;
|
align-items: start;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 4.15rem;
|
min-height: 4.15rem;
|
||||||
background: var(--ui-color-surface-module);
|
background: rgba(6, 8, 7, 0.82);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +297,7 @@
|
||||||
|
|
||||||
.module-card[data-severity="ok"] .icon-glyph {
|
.module-card[data-severity="ok"] .icon-glyph {
|
||||||
background: var(--ui-color-accent);
|
background: var(--ui-color-accent);
|
||||||
color: var(--ui-color-accent-contrast);
|
color: #050605;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-card[data-severity="warning"] {
|
.module-card[data-severity="warning"] {
|
||||||
|
|
@ -368,7 +348,7 @@
|
||||||
|
|
||||||
.service-row:where(a):hover {
|
.service-row:where(a):hover {
|
||||||
border-bottom-color: var(--ui-color-accent);
|
border-bottom-color: var(--ui-color-accent);
|
||||||
background: var(--ui-color-hover);
|
background: rgba(244, 244, 244, 0.035);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-row__main {
|
.service-row__main {
|
||||||
|
|
@ -418,7 +398,7 @@
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
min-height: 1.9rem;
|
min-height: 1.9rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--ui-color-surface-footer);
|
background: rgba(2, 3, 2, 0.92);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
@ -446,7 +426,7 @@
|
||||||
|
|
||||||
.footer-cell[data-severity="ok"] strong {
|
.footer-cell[data-severity="ok"] strong {
|
||||||
background: var(--ui-color-accent);
|
background: var(--ui-color-accent);
|
||||||
color: var(--ui-color-accent-contrast);
|
color: #060706;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-cell[data-severity="warning"] strong {
|
.footer-cell[data-severity="warning"] strong {
|
||||||
|
|
@ -559,48 +539,18 @@
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--ui-color-surface-footer);
|
background: rgba(2, 3, 2, 0.92);
|
||||||
color: var(--ui-color-muted);
|
color: var(--ui-color-muted);
|
||||||
padding: 0;
|
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 {
|
.icon-glyph {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
width: 1.55rem;
|
width: 1.55rem;
|
||||||
height: 1.55rem;
|
height: 1.55rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: var(--ui-border);
|
border: var(--ui-border);
|
||||||
background: var(--ui-color-surface-icon);
|
background: #050605;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -667,7 +617,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-frame__header,
|
.dashboard-frame__header,
|
||||||
.dashboard-frame__title,
|
|
||||||
.dashboard-header,
|
.dashboard-header,
|
||||||
.dashboard-frame__modules,
|
.dashboard-frame__modules,
|
||||||
.dashboard-frame__panels {
|
.dashboard-frame__panels {
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,9 @@ export { SystemState } from "./components/SystemState";
|
||||||
export { TelemetryCard } from "./components/TelemetryCard";
|
export { TelemetryCard } from "./components/TelemetryCard";
|
||||||
export { TelemetryGrid } from "./components/TelemetryGrid";
|
export { TelemetryGrid } from "./components/TelemetryGrid";
|
||||||
export { TelemetryStrip } from "./components/TelemetryStrip";
|
export { TelemetryStrip } from "./components/TelemetryStrip";
|
||||||
export { ThemeToggle } from "./components/ThemeToggle";
|
|
||||||
export { WeatherModule } from "./components/WeatherModule";
|
export { WeatherModule } from "./components/WeatherModule";
|
||||||
export { dashboardPreviewFixtures } from "./fixtures";
|
export { dashboardPreviewFixtures } from "./fixtures";
|
||||||
export { dashboardDocumentToUiDashboard } from "./model-renderer";
|
export { dashboardDocumentToUiDashboard } from "./model-renderer";
|
||||||
export {
|
|
||||||
getNextUiTheme,
|
|
||||||
isUiTheme,
|
|
||||||
persistUiTheme,
|
|
||||||
resolveInitialUiTheme,
|
|
||||||
UI_THEME_STORAGE_KEY,
|
|
||||||
} from "./theme";
|
|
||||||
export type {
|
export type {
|
||||||
UiDashboardPreview,
|
UiDashboardPreview,
|
||||||
UiLink,
|
UiLink,
|
||||||
|
|
@ -48,4 +40,3 @@ export type {
|
||||||
UiStatusItem,
|
UiStatusItem,
|
||||||
UiTelemetryCard,
|
UiTelemetryCard,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
export type { UiTheme } from "./theme";
|
|
||||||
|
|
|
||||||
|
|
@ -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<typeof ThemeToggle>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
|
||||||
|
|
||||||
export const Dark: Story = {};
|
|
||||||
|
|
||||||
export const Light: Story = {
|
|
||||||
args: {
|
|
||||||
theme: "light",
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
theme: "light",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -36,7 +36,6 @@ const requiredStoryFiles = [
|
||||||
"TelemetryCard.stories.tsx",
|
"TelemetryCard.stories.tsx",
|
||||||
"TelemetryGrid.stories.tsx",
|
"TelemetryGrid.stories.tsx",
|
||||||
"TelemetryStrip.stories.tsx",
|
"TelemetryStrip.stories.tsx",
|
||||||
"ThemeToggle.stories.tsx",
|
|
||||||
"WeatherModule.stories.tsx",
|
"WeatherModule.stories.tsx",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
@ -75,13 +74,6 @@ describe("Storybook inventory", () => {
|
||||||
expect(dashboardFrame).not.toContain('./styles.css');
|
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", () => {
|
test("keeps component and story files paired as the UI inventory changes", () => {
|
||||||
const componentStoryFiles = readdirSync(componentsDir)
|
const componentStoryFiles = readdirSync(componentsDir)
|
||||||
.filter((filename) => filename.endsWith(".tsx") && !filename.endsWith(".test.tsx"))
|
.filter((filename) => filename.endsWith(".tsx") && !filename.endsWith(".test.tsx"))
|
||||||
|
|
|
||||||
|
|
@ -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<string, string>();
|
|
||||||
|
|
||||||
expect(resolveInitialUiTheme(storage)).toBe("dark");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restores a valid stored preference", () => {
|
|
||||||
const storage = new Map<string, string>([[UI_THEME_STORAGE_KEY, "light"]]);
|
|
||||||
|
|
||||||
expect(resolveInitialUiTheme(storage)).toBe("light");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ignores invalid stored preferences", () => {
|
|
||||||
const storage = new Map<string, string>([[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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +1,14 @@
|
||||||
:root,
|
:root {
|
||||||
[data-ui-theme="dark"] {
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--ui-color-canvas: #020302;
|
--ui-color-canvas: #020302;
|
||||||
--ui-color-surface: #060706;
|
--ui-color-surface: #060706;
|
||||||
--ui-color-surface-raised: #0b0d0c;
|
--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: rgba(244, 244, 244, 0.16);
|
||||||
--ui-color-line-strong: rgba(244, 244, 244, 0.32);
|
--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-text: #f3f4ed;
|
||||||
--ui-color-muted: #8d948c;
|
--ui-color-muted: #8d948c;
|
||||||
--ui-color-dim: #555b55;
|
--ui-color-dim: #555b55;
|
||||||
--ui-color-accent: #d7ff00;
|
--ui-color-accent: #d7ff00;
|
||||||
--ui-color-accent-contrast: #050605;
|
|
||||||
--ui-color-ok: #bfff00;
|
--ui-color-ok: #bfff00;
|
||||||
--ui-color-warning: #ffb020;
|
--ui-color-warning: #ffb020;
|
||||||
--ui-color-danger: #ff1744;
|
--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);
|
--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 {
|
html {
|
||||||
background: var(--ui-color-canvas);
|
background: var(--ui-color-canvas);
|
||||||
}
|
}
|
||||||
|
|
@ -86,8 +42,8 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background:
|
background:
|
||||||
linear-gradient(var(--ui-color-grid-line) 1px, transparent 1px),
|
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
linear-gradient(90deg, var(--ui-color-grid-line) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
var(--ui-color-canvas);
|
var(--ui-color-canvas);
|
||||||
background-size: 48px 48px, 48px 48px, auto;
|
background-size: 48px 48px, 48px 48px, auto;
|
||||||
color: var(--ui-color-text);
|
color: var(--ui-color-text);
|
||||||
|
|
|
||||||
|
|
@ -209,16 +209,6 @@ test.describe("dashboard page QA gate", () => {
|
||||||
await waitForDashboardReady(page);
|
await waitForDashboardReady(page);
|
||||||
|
|
||||||
await expect(page.getByRole("main")).toHaveCount(1);
|
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) {
|
for (const serviceId of linkedServiceIds) {
|
||||||
await page.keyboard.press("Tab");
|
await page.keyboard.press("Tab");
|
||||||
|
|
||||||
|
|
@ -240,24 +230,6 @@ test.describe("dashboard page QA gate", () => {
|
||||||
expect(results.violations).toEqual([]);
|
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 }) => {
|
test("honors reduced-motion preferences", async ({ page }) => {
|
||||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
|
|
||||||
|
|
@ -283,29 +255,6 @@ test.describe("dashboard page QA gate", () => {
|
||||||
0.01,
|
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<void> {
|
async function waitForDashboardReady(page: Page): Promise<void> {
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 252 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 274 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 273 KiB |
|
|
@ -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}`);
|
|
||||||
|
|
@ -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([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue