diff --git a/README.md b/README.md index 9eca605..d96215b 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ The interface does not execute contracts in the browser. Generated commands run ## Features +- **Theme Variants**: Supports both high-contrast dark theme (default) and WCAG AA/AAA-compliant light theme variant with an accessible navigation toggle button. +- **High-Density Display Refinement**: Subpixel border rendering, crisp box-shadows, and antialiasing tailored for Retina / high-DPI displays. +- **Interactive Module Cards**: Keyboard-accessible (`tabIndex={0}`) module cards with interactive `:hover` and `:focus-visible` outline indicators and accessible descriptions. +- **Consistent Vertical Rhythm**: Standardized section spacing, header margins, and content gutters across all page sections (`hero`, `modules`, `commands`, `footer`). - **Design System & Theme Tokens**: Comprehensive CSS custom property scale (`:root`) standardizing color schemes (canvas, surfaces, accent, text hierarchy, borders) and spacing increments. - **Module Filter & Empty State**: Search and filter testing modules by name or description with real-time feedback and an accessible empty-state layout with quick filter reset. - **Responsive Layout Enhancements**: diff --git a/src/App.test.tsx b/src/App.test.tsx index 05fa906..cf9898e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -65,6 +65,62 @@ describe("App command copy", () => { }); }); +describe("Light theme variant (#202)", () => { + it("defaults to dark theme and toggles to light theme with high contrast", () => { + const { container } = render(); + const main = container.querySelector("main"); + expect(main).toHaveAttribute("data-theme", "dark"); + + const toggleButton = screen.getByRole("button", { + name: "Switch to light theme", + }); + expect(toggleButton).toBeInTheDocument(); + + fireEvent.click(toggleButton); + expect(main).toHaveAttribute("data-theme", "light"); + expect( + screen.getByRole("button", { name: "Switch to dark theme" }), + ).toBeInTheDocument(); + + fireEvent.click(toggleButton); + expect(main).toHaveAttribute("data-theme", "dark"); + }); +}); + +describe("Module card keyboard focus and accessibility (#191)", () => { + it("makes each module card keyboard focusable with descriptive accessible labels", () => { + render(); + const grid = document.getElementById("module-grid") as HTMLElement; + const articles = within(grid).getAllByRole("article"); + expect(articles).toHaveLength(7); + + articles.forEach((article) => { + expect(article).toHaveAttribute("tabindex", "0"); + expect(article).toHaveAttribute("aria-label"); + }); + + const firstCard = articles[0]; + firstCard.focus(); + expect(document.activeElement).toBe(firstCard); + }); +}); + +describe("Section rhythm and page structure (#192)", () => { + it("renders all page sections in a consistent vertical rhythm", () => { + const { container } = render(); + + const hero = container.querySelector("section.hero#top"); + const modules = container.querySelector("section.modules#modules"); + const commands = container.querySelector("section.commands#commands"); + const footer = container.querySelector("footer"); + + expect(hero).toBeInTheDocument(); + expect(modules).toBeInTheDocument(); + expect(commands).toBeInTheDocument(); + expect(footer).toBeInTheDocument(); + }); +}); + describe("Module filter and empty-state layout (#188)", () => { it("renders all 7 modules by default", () => { render(); diff --git a/src/App.tsx b/src/App.tsx index d179133..7d2b8a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -305,6 +305,7 @@ function highlightCommand(command: string): React.ReactNode { } export default function App() { + const [theme, setTheme] = useState<"dark" | "light">("dark"); const [command, setCommand] = useState("coverage"); const [path, setPath] = useState("."); const [format, setFormat] = useState<"html" | "lcov" | "text">("html"); @@ -595,6 +596,10 @@ export default function App() { }, 1600); }; + const toggleTheme = () => { + setTheme((prev) => (prev === "dark" ? "light" : "dark")); + }; + // Issue #144: Copy command with explanatory comments const copyWithComments = async () => { const commented = formatCommandWithComments(options); @@ -685,7 +690,7 @@ export default function App() { const fieldClasses = "field-input"; return ( -
+
Skip to main content @@ -725,6 +730,15 @@ export default function App() { + @@ -912,7 +926,12 @@ export default function App() { {filteredModules.length > 0 ? (
{filteredModules.map((mod, index) => ( -
+