Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand Down
56 changes: 56 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);
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(<App />);
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(<App />);

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(<App />);
Expand Down
23 changes: 21 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandName>("coverage");
const [path, setPath] = useState(".");
const [format, setFormat] = useState<"html" | "lcov" | "text">("html");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -685,7 +690,7 @@ export default function App() {
const fieldClasses = "field-input";

return (
<main>
<main data-theme={theme}>
<a className="skip-link" href="#top">
Skip to main content
</a>
Expand Down Expand Up @@ -725,6 +730,15 @@ export default function App() {
<button type="button" className="nav-a11y-link" onClick={() => { setA11yOpen(true); setMobileMenuOpen(false); }}>
Accessibility
</button>
<button
type="button"
className="theme-toggle"
onClick={toggleTheme}
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} theme`}
>
<span aria-hidden="true">{theme === "dark" ? "☀️" : "🌙"}</span>
<span>{theme === "dark" ? "Light" : "Dark"}</span>
</button>
</div>
</nav>

Expand Down Expand Up @@ -912,7 +926,12 @@ export default function App() {
{filteredModules.length > 0 ? (
<div className="module-grid" id="module-grid">
{filteredModules.map((mod, index) => (
<article key={mod.name} className="module-card">
<article
key={mod.name}
className="module-card"
tabIndex={0}
aria-label={`Module ${mod.name}: ${mod.description}`}
>
<button
type="button"
className="module-card-select"
Expand Down
119 changes: 116 additions & 3 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@
--font-sans: Manrope, sans-serif;
--font-mono: "DM Mono", monospace;

/* Consistent Vertical Rhythm & Spacing Tokens */
--section-spacing: 100px;
--section-header-margin: 50px;
--content-gutter: clamp(22px, 5vw, 72px);

/* Dark Theme (Default) */
--canvas: #061310;
--canvas-gradient: #123a31;
--surface: #091916;
--surface-card: #ffffff02;
--surface-card-hover: #66e2ad0c;
--surface-terminal: #07100e;
--surface-output: #020807;
--accent: #66e2ad;
--accent-bright: #7ff4c2;
--accent-dark: #062017;
--accent-subtle-border: #66e2ad22;
--accent-glow: #66e2ad15;
--accent-tag-bg: #66e2ad0d;
--text-primary: #e9f4ef;
--text-secondary: #a9bbb4;
--text-body: #91a39c;
--text-muted: #6f817a;
--text-number: #587068;
--text-pre: #dbe9e3;
--border-subtle: #ffffff12;
--border-default: #ffffff1c;
--border-strong: #ffffff24;
--border-input: #ffffff1f;
--input-bg: #ffffff06;
--terminal-dot: #294039;

/* Theme Colors */
--color-canvas: #061310;
--color-canvas-gradient: #123a31;
Expand Down Expand Up @@ -82,6 +114,35 @@
font-synthesis: none;
}

/* Light Theme Variant */
[data-theme="light"] {
--canvas: #f8faf9;
--canvas-gradient: #d5e8df;
--surface: #edf3f0;
--surface-card: #ffffff;
--surface-card-hover: #e5f1ec;
--surface-terminal: #07100e;
--surface-output: #020807;
--accent: #0c8257;
--accent-bright: #076b46;
--accent-dark: #ffffff;
--accent-subtle-border: #0c825733;
--accent-glow: #0c825720;
--accent-tag-bg: #0c825714;
--text-primary: #0a1a14;
--text-secondary: #3b5047;
--text-body: #496359;
--text-muted: #647f75;
--text-number: #6f8c82;
--border-subtle: #dbe6e0;
--border-default: #cdd9d2;
--border-strong: #bacac1;
--border-input: #c2d2ca;
--input-bg: #ffffff;
color: var(--text-primary);
background: var(--canvas);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 78px; }
body { margin: 0; min-width: 320px; background: radial-gradient(circle at 75% 8%, var(--color-canvas-gradient) 0, transparent 28rem), var(--color-canvas); }
Expand Down Expand Up @@ -132,6 +193,23 @@ nav {
}
.nav-links a:hover { color: var(--color-accent); }

/* Theme toggle button */
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--surface-card);
color: var(--text-primary);
font-size: .8rem;
cursor: pointer;
transition: border-color .2s, background .2s;
}
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Hero Section */
.hero { min-height: 680px; padding: var(--space-section) var(--page-padding) 80px; display: grid; grid-template-columns: 1.05fr .95fr; align-items: center; gap: 7vw; }
.eyebrow { color: var(--color-accent); text-transform: uppercase; letter-spacing: .14em; font: 500 .72rem var(--font-mono); }
Expand Down Expand Up @@ -189,10 +267,12 @@ h2 { margin: var(--space-md) 0; font-size: clamp(2rem, 4vw, 4rem); letter-spacin

/* Module Grid */
.module-grid { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--color-border-default); border-left: 1px solid var(--color-border-default); }
.module-grid article { min-height: 210px; padding: var(--space-4xl); border-right: 1px solid var(--color-border-default); border-bottom: 1px solid var(--color-border-default); background: var(--color-surface-card); transition: background .2s; }
.module-grid article:hover { background: var(--color-surface-card-hover); }
.module-grid article { position: relative; min-height: 210px; padding: var(--space-4xl); border-right: 1px solid var(--color-border-default); border-bottom: 1px solid var(--color-border-default); background: var(--color-surface-card); transition: background .2s ease, border-color .2s ease, transform .2s ease, box-shadow .2s ease; outline: none; }
.module-grid article:hover { background: var(--color-surface-card-hover); border-color: var(--accent-subtle-border); z-index: 1; }
.module-grid article:focus-visible { background: var(--surface-card-hover); outline: 2px solid var(--accent); outline-offset: -2px; z-index: 2; box-shadow: 0 0 16px var(--accent-glow); }
.module-grid article:focus-visible h3, .module-grid article:hover h3 { color: var(--accent-bright); }
.module-grid span { color: var(--color-text-module-num); font: .7rem var(--font-mono); }
.module-grid h3 { margin: var(--space-6xl) 0 9px; color: var(--color-accent); font: 500 1.1rem var(--font-mono); }
.module-grid h3 { margin: var(--space-6xl) 0 9px; color: var(--color-accent); font: 500 1.1rem var(--font-mono); transition: color .2s ease; }
.module-grid p { margin: 0; color: var(--color-text-body); line-height: 1.5; font-size: .87rem; }
.module-card-select { display: block; width: 100%; padding: 0; border: 0; color: inherit; background: transparent; text-align: left; cursor: pointer; }
.module-card-select:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }
Expand Down Expand Up @@ -322,6 +402,37 @@ input:invalid:focus, select:invalid:focus { border-color: #ff6b6b; box-shadow: 0

footer { display: flex; justify-content: space-between; padding: 34px var(--page-padding); border-top: 1px solid var(--color-border-subtle); color: var(--color-text-muted); font-size: .78rem; }

/* High-Density Display Refinement (#190) */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx) {
:root {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.terminal {
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.65), 0 0 0 0.5px var(--border-default);
}
.terminal-top {
border-bottom-width: 0.5px;
}
.terminal-top i {
box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.15);
}
.module-grid {
border-top-width: 0.5px;
border-left-width: 0.5px;
}
.module-grid article {
border-right-width: 0.5px;
border-bottom-width: 0.5px;
}
.builder, .output, input, select, .command-tabs button {
border-width: 0.5px;
}
.brand-mark {
border-width: 0.5px;
}
}

/* Responsive adjustments */

/* Issue #184: Large-text layout check for hero section */
Expand Down Expand Up @@ -403,6 +514,7 @@ footer { display: flex; justify-content: space-between; padding: 34px var(--page
.module-grid { grid-template-columns: 1fr; }
.row { grid-template-columns: 1fr; }
.actions { flex-direction: column; }
.terminal pre { padding: 22px; font-size: .7rem; }
footer { gap: 20px; }
}

Expand Down Expand Up @@ -1235,3 +1347,4 @@ footer { display: flex; justify-content: space-between; padding: 34px var(--page
margin-top: var(--space-md);
align-self: flex-start;
}
>>>>>>> upstream/main