From e2b769ad590589cf6688dbe99be223a9deef9f8a Mon Sep 17 00:00:00 2001 From: Jaydbrown <175232057+Jaydbrown@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:14:30 +0100 Subject: [PATCH] feat: theme tokens, module filters, responsive navigation, and setting badges - Add comprehensive CSS custom property theme tokens for colors and spacing in styles.css (fixes #193) - Add interactive module search/filter with accessible empty-state layout and reset (fixes #188) - Add responsive reduced-width navigation layout with label truncation (fixes #189) - Add visual distinction badges and aria-required attributes for required vs optional settings (fixes #196) - Add comprehensive test coverage in App.test.tsx - Update README documentation --- README.md | 7 + src/App.test.tsx | 115 +++++++++++++++++ src/App.tsx | 134 +++++++++++++++++--- src/styles.css | 323 ++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 529 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 7995ec8..dcdea19 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 + +- **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 & Reduced-Width Navigation**: Accessible navigation layout supporting truncated and responsive labels without overflowing viewport on mobile or narrow screens. +- **Required vs. Optional Setting Distinction**: Visual badges (`Required` / `Optional`) and `aria-required` attributes in the CLI workbench distinguishing mandatory parameters from optional configuration flags. + ## Development ```sh diff --git a/src/App.test.tsx b/src/App.test.tsx index 8676631..76d1bbd 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -63,3 +63,118 @@ describe("App command copy", () => { expect(await screen.findByRole("button", { name: "Copied" })).toBeInTheDocument(); }); }); + +describe("Module filter and empty-state layout (#188)", () => { + it("renders all 7 modules by default", () => { + render(); + expect(screen.getByText("core")).toBeInTheDocument(); + expect(screen.getByText("ledger")).toBeInTheDocument(); + expect(screen.getByText("money")).toBeInTheDocument(); + expect(screen.getByText("events")).toBeInTheDocument(); + expect(screen.getByText("tokens")).toBeInTheDocument(); + expect(screen.getByText("auth")).toBeInTheDocument(); + expect(screen.getByText("ttl")).toBeInTheDocument(); + }); + + it("filters modules matching search query by name or description", () => { + render(); + const filterInput = screen.getByLabelText("Filter modules"); + + fireEvent.change(filterInput, { target: { value: "ledger" } }); + + expect(screen.getByText("ledger")).toBeInTheDocument(); + expect(screen.queryByText("tokens")).not.toBeInTheDocument(); + expect(screen.queryByText("auth")).not.toBeInTheDocument(); + }); + + it("renders an accessible empty-state layout when no modules match the filter", () => { + render(); + const filterInput = screen.getByLabelText("Filter modules"); + + fireEvent.change(filterInput, { target: { value: "nonexistent-module" } }); + + const emptyState = screen.getByRole("status"); + expect(emptyState).toBeInTheDocument(); + expect(emptyState).toHaveAttribute("aria-live", "polite"); + expect( + screen.getByText(/No modules match/i), + ).toBeInTheDocument(); + + const resetButton = screen.getByRole("button", { name: "Reset filter" }); + expect(resetButton).toBeInTheDocument(); + + fireEvent.click(resetButton); + + expect(screen.getByText("core")).toBeInTheDocument(); + expect(screen.queryByRole("status")).not.toBeInTheDocument(); + }); + + it("clears the filter using the Clear button", () => { + render(); + const filterInput = screen.getByLabelText("Filter modules"); + + fireEvent.change(filterInput, { target: { value: "events" } }); + expect(screen.getByText("events")).toBeInTheDocument(); + expect(screen.queryByText("ttl")).not.toBeInTheDocument(); + + const clearButton = screen.getByRole("button", { name: "Clear module filter" }); + fireEvent.click(clearButton); + + expect(screen.getByText("ttl")).toBeInTheDocument(); + }); +}); + +describe("Reduced-width layout for navigation labels (#189)", () => { + it("renders accessible navigation links with responsive container attributes", () => { + render(); + const nav = screen.getByRole("navigation"); + expect(nav).toBeInTheDocument(); + + const brand = screen.getByLabelText("Soroban Testkit home"); + expect(brand).toBeInTheDocument(); + + const links = screen.getByLabelText("Main navigation"); + expect(links).toBeInTheDocument(); + expect(screen.getByText("Modules")).toHaveAttribute("href", "#modules"); + expect(screen.getByText("Commands")).toHaveAttribute("href", "#commands"); + expect(screen.getByText("GitHub ↗")).toHaveAttribute( + "href", + "https://github.com/soroban-testkit/testkit-blockchain", + ); + }); +}); + +describe("Visual distinction between required and optional settings (#196)", () => { + it("displays Required badge and aria-required on required path input", () => { + render(); + const pathInput = screen.getByLabelText(/project or contract path/i); + expect(pathInput).toHaveAttribute("aria-required", "true"); + expect(screen.getAllByText("Required").length).toBeGreaterThanOrEqual(1); + }); + + it("displays Optional badge on optional format and open options in coverage workflow", () => { + render(); + expect(screen.getByLabelText(/report format/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/open report when complete/i)).toBeInTheDocument(); + expect(screen.getAllByText("Optional").length).toBeGreaterThanOrEqual(2); + }); + + it("displays Required badges and aria-required on function and ramp in limits workflow", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "limits" })); + + const fnInput = screen.getByLabelText(/function/i); + const rampInput = screen.getByLabelText(/ramp parameter/i); + + expect(fnInput).toHaveAttribute("aria-required", "true"); + expect(rampInput).toHaveAttribute("aria-required", "true"); + }); + + it("displays Optional badge on fail on warnings option in audit workflow", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "audit" })); + + expect(screen.getByLabelText(/fail on warnings/i)).toBeInTheDocument(); + expect(screen.getByText("Optional")).toBeInTheDocument(); + }); +}); diff --git a/src/App.tsx b/src/App.tsx index 902695f..0e00718 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,12 +17,22 @@ export default function App() { const [ramp, setRamp] = useState("recipients"); const [strict, setStrict] = useState(true); const [copied, setCopied] = useState(false); + const [moduleFilter, setModuleFilter] = useState(""); const generated = useMemo( () => buildCommand({ command, path, format, open, fn, ramp, strict }), [command, path, format, open, fn, ramp, strict], ); + const filteredModules = useMemo(() => { + const q = moduleFilter.trim().toLowerCase(); + if (!q) return modules; + return modules.filter( + ([name, description]) => + name.toLowerCase().includes(q) || description.toLowerCase().includes(q), + ); + }, [moduleFilter]); + const copy = async () => { await copyToClipboard(generated); setCopied(true); @@ -36,7 +46,7 @@ export default function App() { S Soroban Testkit -
+
Modules Commands GitHub ↗ @@ -64,14 +74,55 @@ 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}

-
- ))} -
+
+

One prelude. Seven focused modules.

+

Everything a serious contract test needs.

+
+ + setModuleFilter(event.target.value)} + aria-label="Filter modules" + /> + {moduleFilter && ( + + )} +
+
+ + {filteredModules.length > 0 ? ( +
+ {filteredModules.map(([name, description], index) => ( +
+ 0{index + 1}

{name}

{description}

+
+ ))} +
+ ) : ( +
+ +

No modules match “{moduleFilter}”

+

No testing modules found matching your filter criteria. Try adjusting your search term.

+ +
+ )}
@@ -81,19 +132,72 @@ export default function App() {

Configure a supported Testkit workflow and copy the exact command to your terminal.

{(["coverage", "limits", "audit"] as const).map((name) => ( - + ))}

{descriptions[command]}

- + {command === "coverage" && <> - - + + } - {command === "limits" &&
} - {command === "audit" && } + {command === "limits" &&
+ + +
} + {command === "audit" && }
{generated}
diff --git a/src/styles.css b/src/styles.css index 332b692..258df30 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,42 +1,295 @@ @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 { + /* Typography */ + --font-sans: Manrope, sans-serif; + --font-mono: "DM Mono", monospace; + + /* Theme Colors */ + --color-canvas: #061310; + --color-canvas-gradient: #123a31; + --color-surface: #091916; + --color-surface-terminal: #07100e; + --color-surface-output: #020807; + --color-surface-card: #ffffff02; + --color-surface-card-hover: #66e2ad08; + + /* Accent Colors */ + --color-accent: #66e2ad; + --color-accent-text-dark: #062017; + --color-accent-subtle: #66e2ad0b; + --color-accent-border: #66e2ad22; + --color-accent-surface: #66e2ad0d; + + /* Text Colors */ + --color-text-primary: #e9f4ef; + --color-text-secondary: #a9bbb4; + --color-text-muted: #6f817a; + --color-text-body: #91a39c; + --color-text-label: #a7b8b1; + --color-text-tab: #8ea099; + --color-text-code: #dbe9e3; + --color-text-module-num: #587068; + + /* Border Colors */ + --color-border-subtle: #ffffff12; + --color-border-default: #ffffff1c; + --color-border-strong: #ffffff24; + --color-border-input: #ffffff1f; + --color-border-tab: #ffffff20; + + /* Badges */ + --color-badge-required-bg: #66e2ad1a; + --color-badge-required-text: #66e2ad; + --color-badge-required-border: #66e2ad33; + --color-badge-optional-bg: #ffffff10; + --color-badge-optional-text: #8ea099; + --color-badge-optional-border: #ffffff1a; + + /* Form Elements */ + --color-input-bg: #ffffff06; + --color-terminal-dot: #294039; + + /* Spacing Scale */ + --space-3xs: 2px; + --space-2xs: 4px; + --space-xs: 7px; + --space-sm: 8px; + --space-md: 12px; + --space-base: 14px; + --space-lg: 16px; + --space-xl: 18px; + --space-2xl: 22px; + --space-3xl: 24px; + --space-4xl: 28px; + --space-5xl: 32px; + --space-6xl: 48px; + --space-section: 100px; + --page-padding: 5vw; + + /* Radii */ + --radius-xs: 4px; + --radius-sm: 5px; + --radius-md: 6px; + --radius-base: 7px; + --radius-lg: 10px; + --radius-xl: 12px; + --radius-round: 50%; + + font-family: var(--font-sans); + color: var(--color-text-primary); + background: var(--color-canvas); + font-synthesis: none; +} + * { 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(--color-canvas-gradient) 0, transparent 28rem), var(--color-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; } -.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"; } -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; } -.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; } } + +/* Navigation with reduced-width adaptation */ +nav { + height: 78px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--page-padding); + border-bottom: 1px solid var(--color-border-subtle); +} +.brand { + display: flex; + align-items: center; + gap: var(--space-md); + font-weight: 700; + flex-shrink: 0; +} +.brand-mark { + display: grid; + place-items: center; + width: 34px; + height: 34px; + border: 1px solid var(--color-accent); + border-radius: var(--radius-round); + color: var(--color-accent); + font-family: var(--font-mono); +} +.nav-links { + display: flex; + align-items: center; + gap: clamp(14px, 2.5vw, 30px); + color: var(--color-text-secondary); + font-size: .9rem; +} +.nav-links a { + display: inline-flex; + align-items: center; + white-space: nowrap; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; +} +.nav-links a:hover { color: var(--color-accent); } + +/* 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); } +h1 { margin: var(--space-xl) 0 var(--space-3xl); font-size: clamp(3.4rem, 6.5vw, 7rem); line-height: .96; letter-spacing: -.065em; } +.lede { max-width: 650px; color: var(--color-text-secondary); font-size: 1.15rem; line-height: 1.75; } +.actions { display: flex; gap: var(--space-md); margin-top: 38px; } +.button { padding: 13px 19px; border-radius: var(--radius-base); font-weight: 600; font-size: .9rem; cursor: pointer; } +.primary { color: var(--color-accent-text-dark); background: var(--color-accent); border: 0; } +.secondary { border: 1px solid var(--color-border-strong); background: transparent; color: var(--color-text-primary); } + +/* Terminal */ +.terminal { background: var(--color-surface-terminal); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); 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: var(--space-xs); padding: 0 var(--space-xl); border-bottom: 1px solid var(--color-border-subtle); color: var(--color-text-muted); font-size: .7rem; } +.terminal-top i { width: 8px; height: 8px; border-radius: var(--radius-round); background: var(--color-terminal-dot); } +.terminal-top span { margin-left: auto; } +.terminal pre { margin: 0; padding: 34px; color: var(--color-text-code); font-size: .85rem; line-height: 1.85; overflow: auto; } +.terminal b, .muted { color: var(--color-accent); font-weight: 400; } +.pass { padding: var(--space-base) var(--space-3xl); background: var(--color-accent-surface); border-top: 1px solid var(--color-accent-border); color: var(--color-accent); font-size: .75rem; } + +/* Modules */ +.modules, .commands { padding: var(--space-section) var(--page-padding); border-top: 1px solid var(--color-border-subtle); } +section header { max-width: 650px; margin-bottom: 50px; } +h2 { margin: var(--space-md) 0; font-size: clamp(2rem, 4vw, 4rem); letter-spacing: -.045em; line-height: 1.05; } + +/* Module filter bar */ +.module-filter-bar { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-top: var(--space-xl); + max-width: 480px; +} +.module-filter-input { + width: 100%; + padding: 10px 14px; + background: var(--color-input-bg); + border: 1px solid var(--color-border-input); + border-radius: var(--radius-sm); + color: var(--color-text-primary); + font-size: .85rem; + outline: none; +} +.module-filter-input:focus { border-color: var(--color-accent); } +.module-filter-clear { + padding: 10px 14px; + background: transparent; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + font-size: .8rem; + white-space: nowrap; +} +.module-filter-clear:hover { color: var(--color-accent); border-color: var(--color-accent); } + +/* 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 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 p { margin: 0; color: var(--color-text-body); line-height: 1.5; font-size: .87rem; } + +/* Module Empty State */ +.module-empty-state { + padding: 60px var(--space-3xl); + text-align: center; + background: var(--color-surface-card); + border: 1px dashed var(--color-border-strong); + border-radius: var(--radius-lg); + margin-top: var(--space-xl); +} +.module-empty-icon { font-size: 2rem; color: var(--color-text-muted); margin-bottom: var(--space-md); font-family: var(--font-mono); } +.module-empty-state h3 { margin: 0 0 var(--space-sm); font-size: 1.2rem; color: var(--color-text-primary); } +.module-empty-state p { margin: 0 0 var(--space-xl); color: var(--color-text-secondary); font-size: .9rem; line-height: 1.6; } +.module-empty-state button { display: inline-block; } + +/* Commands / CLI Workbench */ +.commands { display: grid; grid-template-columns: .8fr 1.2fr; gap: 8vw; background: var(--color-surface); } +.command-copy > p:not(.eyebrow) { color: var(--color-text-body); line-height: 1.7; } +.command-tabs { display: flex; gap: var(--space-sm); margin-top: 36px; } +.command-tabs button { border: 1px solid var(--color-border-tab); color: var(--color-text-tab); background: transparent; padding: 9px var(--space-base); border-radius: var(--radius-sm); cursor: pointer; font: .75rem var(--font-mono); } +.command-tabs button.active { border-color: var(--color-accent); color: var(--color-accent); background: var(--color-accent-subtle); } +.description { min-height: 48px; } +.builder { padding: var(--space-5xl); background: var(--color-canvas); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); } + +/* Field & Required/Optional distinction */ +label { display: grid; gap: 9px; margin-bottom: var(--space-2xl); color: var(--color-text-label); font-size: .78rem; } +.field-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-sm); +} +.badge { + display: inline-flex; + align-items: center; + padding: var(--space-3xs) var(--space-xs); + border-radius: var(--radius-xs); + font-size: .65rem; + font-family: var(--font-mono); + font-weight: 500; + letter-spacing: .08em; + text-transform: uppercase; +} +.badge-required { + background: var(--color-badge-required-bg); + color: var(--color-badge-required-text); + border: 1px solid var(--color-badge-required-border); +} +.badge-optional { + background: var(--color-badge-optional-bg); + color: var(--color-badge-optional-text); + border: 1px solid var(--color-badge-optional-border); +} + +.row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-xl); } +input, select { width: 100%; padding: var(--space-md) 13px; color: var(--color-text-primary); background: var(--color-input-bg); border: 1px solid var(--color-border-input); border-radius: var(--radius-sm); outline: none; } +input:focus, select:focus { border-color: var(--color-accent); } +.check { display: flex; align-items: center; justify-content: space-between; } +.check-content { display: flex; align-items: center; gap: var(--space-sm); } +.check input { width: auto; accent-color: var(--color-accent); } +.output { display: flex; align-items: center; gap: 15px; padding: var(--space-lg); background: var(--color-surface-output); border-radius: var(--radius-md); } +.output code { flex: 1; color: var(--color-accent); font: .75rem/1.6 var(--font-mono); overflow-wrap: anywhere; } +.output button { color: var(--color-canvas); background: var(--color-accent); border: 0; border-radius: var(--radius-xs); padding: var(--space-sm) var(--space-md); cursor: pointer; font-weight: 600; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 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; } + +/* Responsive adjustments */ +@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 { max-width: 140px; } +} + +@media (max-width: 600px) { + nav { + height: auto; + min-height: 64px; + padding: var(--space-md) var(--page-padding); + flex-wrap: wrap; + gap: var(--space-md); + } + .nav-links { + gap: var(--space-md); + font-size: .85rem; + flex-wrap: wrap; + } + .nav-links a { max-width: 120px; } +} + +@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; } +}