diff --git a/README.md b/README.md index 7995ec8..6dd6c6d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ A visual companion to [Soroban Testkit](https://github.com/soroban-testkit/testk The interface does not execute contracts in the browser. Generated commands run locally through `soroban-testkit`, keeping contract tests deterministic and offline as required by the core project. +## 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`). + ## Development ```sh diff --git a/src/App.test.tsx b/src/App.test.tsx index 8676631..160cab2 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -63,3 +63,58 @@ describe("App command copy", () => { expect(await screen.findByRole("button", { name: "Copied" })).toBeInTheDocument(); }); }); + +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 articles = screen.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(); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index 902695f..8353965 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ const descriptions: Record = { }; 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"); @@ -29,8 +30,12 @@ export default function App() { window.setTimeout(() => setCopied(false), 1600); }; + const toggleTheme = () => { + setTheme((prev) => (prev === "dark" ? "light" : "dark")); + }; + return ( -
+
@@ -67,7 +81,11 @@ export default function App() {

One prelude. Seven focused modules.

Everything a serious contract test needs.

{modules.map(([name, description], index) => ( -
+
0{index + 1}

{name}

{description}

))} diff --git a/src/styles.css b/src/styles.css index 332b692..b409586 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,42 +1,246 @@ @import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap"); -:root { font-family: Manrope, sans-serif; color: #e9f4ef; background: #061310; font-synthesis: none; } +:root { + --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; + + font-family: var(--font-sans); + color: var(--text-primary); + background: var(--canvas); + 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; } -body { margin: 0; min-width: 320px; background: radial-gradient(circle at 75% 8%, #123a31 0, transparent 28rem), #061310; } +body { margin: 0; min-width: 320px; background: radial-gradient(circle at 75% 8%, var(--canvas-gradient) 0, transparent 28rem), var(--canvas); } a { color: inherit; text-decoration: none; } button, input, select { font: inherit; } main { max-width: 1440px; margin: auto; overflow: hidden; } -nav { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 5vw; border-bottom: 1px solid #ffffff12; } + +/* Navigation */ +nav { + height: 78px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--content-gutter); + border-bottom: 1px solid var(--border-subtle); +} .brand { display: flex; align-items: center; gap: 12px; font-weight: 700; } -.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid #66e2ad; border-radius: 50%; color: #66e2ad; font-family: "DM Mono"; } -.nav-links { display: flex; gap: 30px; color: #a9bbb4; font-size: .9rem; } -.nav-links a:hover { color: #66e2ad; } -.hero { min-height: 680px; padding: 100px 5vw 80px; display: grid; grid-template-columns: 1.05fr .95fr; align-items: center; gap: 7vw; } -.eyebrow { color: #66e2ad; text-transform: uppercase; letter-spacing: .14em; font: 500 .72rem "DM Mono"; } +.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--accent); border-radius: 50%; color: var(--accent); font-family: var(--font-mono); } +.nav-links { display: flex; align-items: center; gap: 24px; color: var(--text-secondary); font-size: .9rem; } +.nav-links a:hover { color: var(--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; } + +/* Vertical Rhythm - Page Sections */ +.hero { + min-height: 680px; + padding: var(--section-spacing) var(--content-gutter) 80px; + display: grid; + grid-template-columns: 1.05fr .95fr; + align-items: center; + gap: 7vw; +} +.modules, .commands { + padding: var(--section-spacing) var(--content-gutter); + border-top: 1px solid var(--border-subtle); +} +section header { + max-width: 650px; + margin-bottom: var(--section-header-margin); +} + +.eyebrow { color: var(--accent); text-transform: uppercase; letter-spacing: .14em; font: 500 .72rem var(--font-mono); } h1 { margin: 18px 0 24px; font-size: clamp(3.4rem, 6.5vw, 7rem); line-height: .96; letter-spacing: -.065em; } -.lede { max-width: 650px; color: #a9bbb4; font-size: 1.15rem; line-height: 1.75; } +.lede { max-width: 650px; color: var(--text-secondary); font-size: 1.15rem; line-height: 1.75; } .actions { display: flex; gap: 12px; margin-top: 38px; } -.button { padding: 13px 19px; border-radius: 7px; font-weight: 600; font-size: .9rem; } -.primary { color: #062017; background: #66e2ad; } -.secondary { border: 1px solid #ffffff24; } -.terminal { background: #07100e; border: 1px solid #ffffff1c; border-radius: 12px; box-shadow: 0 30px 80px #0008; transform: rotate(1deg); overflow: hidden; font-family: "DM Mono"; } -.terminal-top { height: 48px; display: flex; align-items: center; gap: 7px; padding: 0 18px; border-bottom: 1px solid #ffffff12; color: #6f817a; font-size: .7rem; } -.terminal-top i { width: 8px; height: 8px; border-radius: 50%; background: #294039; }.terminal-top span { margin-left: auto; } -.terminal pre { margin: 0; padding: 34px; color: #dbe9e3; font-size: .85rem; line-height: 1.85; overflow: auto; }.terminal b, .muted { color: #66e2ad; font-weight: 400; } -.pass { padding: 14px 24px; background: #66e2ad0d; border-top: 1px solid #66e2ad22; color: #66e2ad; font-size: .75rem; } -.modules, .commands { padding: 100px 5vw; border-top: 1px solid #ffffff12; } -section header { max-width: 650px; margin-bottom: 50px; } h2 { margin: 12px 0; font-size: clamp(2rem, 4vw, 4rem); letter-spacing: -.045em; line-height: 1.05; } -.module-grid { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid #ffffff1c; border-left: 1px solid #ffffff1c; } -.module-grid article { min-height: 210px; padding: 28px; border-right: 1px solid #ffffff1c; border-bottom: 1px solid #ffffff1c; background: #ffffff02; transition: background .2s; }.module-grid article:hover { background: #66e2ad08; } -.module-grid span { color: #587068; font: .7rem "DM Mono"; }.module-grid h3 { margin: 48px 0 9px; color: #66e2ad; font: 500 1.1rem "DM Mono"; }.module-grid p { margin: 0; color: #91a39c; line-height: 1.5; font-size: .87rem; } -.commands { display: grid; grid-template-columns: .8fr 1.2fr; gap: 8vw; background: #091916; }.command-copy > p:not(.eyebrow) { color: #91a39c; line-height: 1.7; } -.command-tabs { display: flex; gap: 8px; margin-top: 36px; }.command-tabs button { border: 1px solid #ffffff20; color: #8ea099; background: transparent; padding: 9px 14px; border-radius: 5px; cursor: pointer; font: .75rem "DM Mono"; }.command-tabs button.active { border-color: #66e2ad; color: #66e2ad; background: #66e2ad0b; } -.description { min-height: 48px; }.builder { padding: 32px; background: #061310; border: 1px solid #ffffff18; border-radius: 10px; } -label { display: grid; gap: 9px; margin-bottom: 22px; color: #a7b8b1; font-size: .78rem; }.row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; } -input, select { width: 100%; padding: 12px 13px; color: #eaf6f1; background: #ffffff06; border: 1px solid #ffffff1f; border-radius: 5px; outline: none; } input:focus, select:focus { border-color: #66e2ad; } -.check { display: flex; align-items: center; grid-template-columns: auto 1fr; }.check input { width: auto; accent-color: #66e2ad; } -.output { display: flex; align-items: center; gap: 15px; padding: 16px; background: #020807; border-radius: 6px; }.output code { flex: 1; color: #66e2ad; font: .75rem/1.6 "DM Mono"; overflow-wrap: anywhere; }.output button { color: #061310; background: #66e2ad; border: 0; border-radius: 4px; padding: 8px 12px; cursor: pointer; } -footer { display: flex; justify-content: space-between; padding: 34px 5vw; border-top: 1px solid #ffffff12; color: #6f817a; font-size: .78rem; } -@media (max-width: 900px) { .hero, .commands { grid-template-columns: 1fr; }.hero { padding-top: 70px; }.module-grid { grid-template-columns: repeat(2, 1fr); }.nav-links a:not(:last-child) { display: none; } } -@media (max-width: 540px) { nav, .hero, .modules, .commands { padding-left: 22px; padding-right: 22px; }.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; } } +.button { padding: 13px 19px; border-radius: 7px; font-weight: 600; font-size: .9rem; cursor: pointer; } +.primary { color: var(--accent-dark); background: var(--accent); border: 0; } +.secondary { border: 1px solid var(--border-strong); background: transparent; color: var(--text-primary); } + +/* Terminal */ +.terminal { background: var(--surface-terminal); border: 1px solid var(--border-default); border-radius: 12px; box-shadow: 0 30px 80px #0008; transform: rotate(1deg); overflow: hidden; font-family: var(--font-mono); } +.terminal-top { height: 48px; display: flex; align-items: center; gap: 7px; padding: 0 18px; border-bottom: 1px solid var(--border-subtle); color: var(--text-muted); font-size: .7rem; } +.terminal-top i { width: 8px; height: 8px; border-radius: 50%; background: var(--terminal-dot); } +.terminal-top span { margin-left: auto; } +.terminal pre { margin: 0; padding: 34px; color: var(--text-pre); font-size: .85rem; line-height: 1.85; overflow: auto; } +.terminal b, .muted { color: var(--accent); font-weight: 400; } +.pass { padding: 14px 24px; background: var(--accent-tag-bg); border-top: 1px solid var(--accent-subtle-border); color: var(--accent); font-size: .75rem; } + +/* Modules Grid with Interactive Hover & Focus States (#191) */ +h2 { margin: 12px 0; font-size: clamp(2rem, 4vw, 4rem); letter-spacing: -.045em; line-height: 1.05; } +.module-grid { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--border-default); border-left: 1px solid var(--border-default); } +.module-grid article { + position: relative; + min-height: 210px; + padding: 28px; + border-right: 1px solid var(--border-default); + border-bottom: 1px solid var(--border-default); + background: var(--surface-card); + transition: background .2s ease, border-color .2s ease, transform .2s ease, box-shadow .2s ease; + outline: none; + cursor: default; +} +.module-grid article:hover { + background: var(--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(--text-number); font: .7rem var(--font-mono); } +.module-grid h3 { margin: 48px 0 9px; color: var(--accent); font: 500 1.1rem var(--font-mono); transition: color .2s ease; } +.module-grid p { margin: 0; color: var(--text-body); line-height: 1.5; font-size: .87rem; } + +/* Commands / CLI Workbench */ +.commands { display: grid; grid-template-columns: .8fr 1.2fr; gap: 8vw; background: var(--surface); } +.command-copy > p:not(.eyebrow) { color: var(--text-body); line-height: 1.7; } +.command-tabs { display: flex; gap: 8px; margin-top: 36px; } +.command-tabs button { border: 1px solid var(--border-strong); color: var(--text-secondary); background: transparent; padding: 9px 14px; border-radius: 5px; cursor: pointer; font: .75rem var(--font-mono); } +.command-tabs button.active { border-color: var(--accent); color: var(--accent); background: var(--accent-tag-bg); } +.description { min-height: 48px; } +.builder { padding: 32px; background: var(--canvas); border: 1px solid var(--border-default); border-radius: 10px; } +label { display: grid; gap: 9px; margin-bottom: 22px; color: var(--text-secondary); font-size: .78rem; } +.row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; } +input, select { width: 100%; padding: 12px 13px; color: var(--text-primary); background: var(--input-bg); border: 1px solid var(--border-input); border-radius: 5px; outline: none; } +input:focus, select:focus { border-color: var(--accent); } +.check { display: flex; align-items: center; grid-template-columns: auto 1fr; } +.check input { width: auto; accent-color: var(--accent); } +.output { display: flex; align-items: center; gap: 15px; padding: 16px; background: var(--surface-output); border-radius: 6px; } +.output code { flex: 1; color: var(--accent); font: .75rem/1.6 var(--font-mono); overflow-wrap: anywhere; } +.output button { color: var(--accent-dark); background: var(--accent); border: 0; border-radius: 4px; padding: 8px 12px; cursor: pointer; font-weight: 600; } +footer { display: flex; justify-content: space-between; padding: 34px var(--content-gutter); border-top: 1px solid var(--border-subtle); color: var(--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 queries */ +@media (max-width: 900px) { + .hero, .commands { grid-template-columns: 1fr; } + .hero { padding-top: 70px; } + .module-grid { grid-template-columns: repeat(2, 1fr); } + .nav-links a:not(:last-child) { display: none; } +} + +@media (max-width: 540px) { + nav, .hero, .modules, .commands { padding-left: 22px; padding-right: 22px; } + .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; } +}