inspector/clients/web/.storybook/preview.tsx
cliffhall 04b5e70a53 fix(a11y): make the form error color meet WCAG AA in both schemes
Mantine's `--mantine-color-error` defaults to `red-6` in light and `red-8` in
dark. Both fail the 4.5:1 AA threshold that applies at the 12px size it renders
input error text: 4.48:1 on white, and 1.93:1 on our charcoal body. Override to
`red-7` (5.72:1) and `red-4` (6.44:1) — the latter matching the one-to-two-step
lift App.css already applies to the semantic status/log reds for dark.

Surfaced by the JsonObjectInput stories, which are the first in the repo to
render an input error state and so the first to put this in front of the
Storybook a11y gate. It is app-wide, not specific to that component — every
form error in the Inspector was affected.

It has to go through `cssVariablesResolver`: MantineProvider injects these
variables into a `<style>` block appended after the stylesheet imports, so a
`:root` rule in App.css loses to it on source order at equal specificity.

That resolver was duplicated in main.tsx and .storybook/preview.tsx and missing
from renderWithMantine, so the three could drift on any token they set. Extract
one `theme/cssVariables.ts` and use it at all three sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-23 12:56:13 -04:00

99 lines
3 KiB
TypeScript

import type { Preview } from "@storybook/react-vite";
import { MantineProvider, useMantineColorScheme } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css";
import "../src/App.css";
import { theme } from "../src/theme/theme";
import { cssVariablesResolver } from "../src/theme/cssVariables";
import { useEffect } from "react";
// eslint-disable-next-line react-refresh/only-export-components
function ColorSchemeWrapper({
colorScheme,
children,
}: {
colorScheme: "light" | "dark";
children: React.ReactNode;
}) {
const { setColorScheme } = useMantineColorScheme();
useEffect(() => {
setColorScheme(colorScheme);
}, [colorScheme, setColorScheme]);
return <>{children}</>;
}
const preview: Preview = {
globalTypes: {
colorScheme: {
description: "Mantine color scheme",
toolbar: {
title: "Color Scheme",
icon: "mirror",
items: [
{ value: "light", title: "Light", icon: "sun" },
{ value: "dark", title: "Dark", icon: "moon" },
],
dynamicTitle: true,
},
},
},
initialGlobals: {
colorScheme: "light",
},
decorators: [
(Story, context) => {
const isFullscreen = context.parameters?.layout === "fullscreen";
return (
<MantineProvider
theme={theme}
defaultColorScheme="light"
cssVariablesResolver={cssVariablesResolver}
>
<ColorSchemeWrapper
colorScheme={context.globals.colorScheme ?? "light"}
>
<Notifications position="top-right" />
{isFullscreen ? (
<div style={{ height: "100vh", overflow: "hidden" }}>
<Story />
</div>
) : (
<Story />
)}
</ColorSchemeWrapper>
</MantineProvider>
);
},
],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// `'error'` fails the Storybook play-function tests (part of `npm run ci`)
// on any axe violation, so the zero-violation state this PR reached is
// enforced going forward rather than being a point-in-time result.
test: "error",
// Stories render presentational components (and whole screens) in
// isolation, outside the app's `AppShell` — which is what provides the
// page landmarks (`<main>`, nav) in the real app. The `region` rule
// ("all content must live inside a landmark") is therefore a page-level
// concern that can't be satisfied in Storybook isolation and would fire on
// essentially every story; it's covered by the app shell at runtime, not
// by the components under test. Disable it here so the a11y panel surfaces
// only rules the components can actually own.
config: {
rules: [{ id: "region", enabled: false }],
},
},
layout: "centered",
},
};
export default preview;