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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

- **Shared Design Tokens**: CSS custom properties for surfaces (`--surface-base`, `--surface-elevated`, `--surface-terminal`, `--surface-output`, `--surface-card`) and borders (`--border-subtle`, `--border-default`, `--border-strong`, `--border-input`).
- **Compact Output for 320px Screens**: Responsive mobile output layout stacking the code block and action buttons on viewports down to 320px to prevent horizontal overflow and enhance touch targets.
- **Responsive Footer Layout**: Semantic multi-column footer with wrapping layout displaying brand description, workflow navigation links, and licensing metadata.
- **Workflow Input Reset**: Easily reset customized command builder inputs back to standard workflow defaults via the `Reset` action.

## Development

```sh
Expand Down
90 changes: 90 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,93 @@ describe("App command copy", () => {
expect(await screen.findByRole("button", { name: "Copied" })).toBeInTheDocument();
});
});

describe("Reset behavior after customized inputs (#205)", () => {
it("resets coverage inputs back to defaults after customization", () => {
render(<App />);

const pathInput = screen.getByLabelText(/project or contract path/i) as HTMLInputElement;
const formatSelect = screen.getByLabelText(/report format/i) as HTMLSelectElement;
const openCheckbox = screen.getByLabelText(/open report when complete/i) as HTMLInputElement;

fireEvent.change(pathInput, { target: { value: "./contracts/escrow" } });
fireEvent.change(formatSelect, { target: { value: "lcov" } });
fireEvent.click(openCheckbox); // uncheck

expect(
screen.getByText("soroban-testkit coverage ./contracts/escrow --format lcov"),
).toBeInTheDocument();

const resetButton = screen.getByRole("button", { name: "Reset" });
fireEvent.click(resetButton);

expect(pathInput.value).toBe(".");
expect(formatSelect.value).toBe("html");
expect(openCheckbox.checked).toBe(true);
expect(
screen.getByText("soroban-testkit coverage . --format html --open"),
).toBeInTheDocument();
});

it("resets limits inputs back to defaults after customization", () => {
render(<App />);

fireEvent.click(screen.getByRole("button", { name: "limits" }));

const pathInput = screen.getByLabelText(/project or contract path/i) as HTMLInputElement;
const fnInput = screen.getByLabelText(/function/i) as HTMLInputElement;
const rampInput = screen.getByLabelText(/ramp parameter/i) as HTMLInputElement;

fireEvent.change(pathInput, { target: { value: "./custom.wasm" } });
fireEvent.change(fnInput, { target: { value: "custom_fn" } });
fireEvent.change(rampInput, { target: { value: "custom_ramp" } });

expect(
screen.getByText(
"soroban-testkit limits --contract ./custom.wasm --fn custom_fn --ramp custom_ramp",
),
).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Reset" }));

expect(pathInput.value).toBe(".");
expect(fnInput.value).toBe("batch_payout");
expect(rampInput.value).toBe("recipients");
expect(
screen.getByText(
"soroban-testkit limits --contract . --fn batch_payout --ramp recipients",
),
).toBeInTheDocument();
});

it("resets audit inputs back to defaults after customization", () => {
render(<App />);

fireEvent.click(screen.getByRole("button", { name: "audit" }));

const pathInput = screen.getByLabelText(/project or contract path/i) as HTMLInputElement;
const strictCheckbox = screen.getByLabelText(/fail on warnings/i) as HTMLInputElement;

fireEvent.change(pathInput, { target: { value: "./contracts/vault" } });
fireEvent.click(strictCheckbox); // uncheck strict

expect(screen.getByText("soroban-testkit audit ./contracts/vault")).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Reset" }));

expect(pathInput.value).toBe(".");
expect(strictCheckbox.checked).toBe(true);
expect(screen.getByText("soroban-testkit audit . --strict")).toBeInTheDocument();
});
});

describe("Responsive footer layout (#195)", () => {
it("renders wrapping footer columns with links and metadata", () => {
render(<App />);

expect(screen.getByText("Soroban Testkit", { selector: ".footer-brand" })).toBeInTheDocument();
expect(screen.getByText("Built for the Stellar developer ecosystem.")).toBeInTheDocument();
expect(screen.getByText("Apache-2.0")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "CLI Workbench" })).toBeInTheDocument();
});
});
36 changes: 34 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export default function App() {
[command, path, format, open, fn, ramp, strict],
);

const handleReset = () => {
setPath(".");
setFormat("html");
setOpen(true);
setFn("batch_payout");
setRamp("recipients");
setStrict(true);
};

const copy = async () => {
await copyToClipboard(generated);
setCopied(true);
Expand Down Expand Up @@ -94,11 +103,34 @@ export default function App() {
</>}
{command === "limits" && <div className="row"><label>Function<input value={fn} onChange={(event) => setFn(event.target.value)} /></label><label>Ramp parameter<input value={ramp} onChange={(event) => setRamp(event.target.value)} /></label></div>}
{command === "audit" && <label className="check"><input type="checkbox" checked={strict} onChange={(event) => setStrict(event.target.checked)} /> Fail on warnings</label>}
<div className="output"><code>{generated}</code><button onClick={copy}>{copied ? "Copied" : "Copy"}</button></div>
<div className="output">
<code>{generated}</code>
<div className="output-actions">
<button type="button" className="button-reset" onClick={handleReset}>Reset</button>
<button type="button" onClick={copy}>{copied ? "Copied" : "Copy"}</button>
</div>
</div>
</div>
</section>

<footer><span>Built for the Stellar developer ecosystem.</span><span>Apache-2.0</span></footer>
<footer>
<div className="footer-col">
<span className="footer-brand">Soroban Testkit</span>
<p>Built for the Stellar developer ecosystem.</p>
</div>
<div className="footer-col">
<span className="footer-heading">Workflows</span>
<div className="footer-links">
<a href="#modules">Modules</a>
<a href="#commands">CLI Workbench</a>
<a href="https://github.com/soroban-testkit/testkit-blockchain">Core Testkit ↗</a>
</div>
</div>
<div className="footer-col footer-meta">
<span>Apache-2.0</span>
<span>Deterministic contract testing</span>
</div>
</footer>
</main>
);
}
186 changes: 158 additions & 28 deletions src/styles.css
Original file line number Diff line number Diff line change
@@ -1,42 +1,172 @@
@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 {
/* Shared Surface Tokens (#197) */
--surface-base: #061310;
--surface-elevated: #091916;
--surface-terminal: #07100e;
--surface-output: #020807;
--surface-card: #ffffff02;
--surface-card-hover: #66e2ad08;

/* Shared Border Tokens (#197) */
--border-subtle: #ffffff12;
--border-default: #ffffff1c;
--border-strong: #ffffff24;
--border-input: #ffffff1f;
--border-tab: #ffffff20;

/* Accent & State Tokens */
--color-accent: #66e2ad;
--color-accent-dark: #062017;
--color-accent-subtle: #66e2ad0b;
--color-accent-border: #66e2ad22;
--color-accent-surface: #66e2ad0d;

/* Text Hierarchy Tokens */
--text-primary: #e9f4ef;
--text-secondary: #a9bbb4;
--text-muted: #6f817a;
--text-body: #91a39c;
--text-label: #a7b8b1;
--text-tab: #8ea099;
--text-code: #dbe9e3;
--text-num: #587068;

font-family: Manrope, sans-serif;
color: var(--text-primary);
background: var(--surface-base);
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%, #123a31 0, transparent 28rem), var(--surface-base); }
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; }
nav { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 5vw; 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; }
.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--color-accent); border-radius: 50%; color: var(--color-accent); font-family: "DM Mono"; }
.nav-links { display: flex; gap: 30px; color: var(--text-secondary); font-size: .9rem; }
.nav-links a:hover { color: var(--color-accent); }
.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"; }
.eyebrow { color: var(--color-accent); 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; }
.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; }
.button { padding: 13px 19px; border-radius: 7px; font-weight: 600; font-size: .9rem; cursor: pointer; }
.primary { color: var(--color-accent-dark); background: var(--color-accent); }
.secondary { border: 1px solid var(--border-strong); }
.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: "DM 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: #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; }
.terminal pre { margin: 0; padding: 34px; color: var(--text-code); font-size: .85rem; line-height: 1.85; overflow: auto; }.terminal b, .muted { color: var(--color-accent); font-weight: 400; }
.pass { padding: 14px 24px; background: var(--color-accent-surface); border-top: 1px solid var(--color-accent-border); color: var(--color-accent); font-size: .75rem; }
.modules, .commands { padding: 100px 5vw; border-top: 1px solid var(--border-subtle); }
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; }
.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 { 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; }.module-grid article:hover { background: var(--surface-card-hover); }
.module-grid span { color: var(--text-num); font: .7rem "DM Mono"; }.module-grid h3 { margin: 48px 0 9px; color: var(--color-accent); font: 500 1.1rem "DM Mono"; }.module-grid p { margin: 0; color: var(--text-body); line-height: 1.5; font-size: .87rem; }
.commands { display: grid; grid-template-columns: .8fr 1.2fr; gap: 8vw; background: var(--surface-elevated); }.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-tab); color: var(--text-tab); background: transparent; padding: 9px 14px; border-radius: 5px; cursor: pointer; font: .75rem "DM 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: 32px; background: var(--surface-base); border: 1px solid var(--border-default); border-radius: 10px; }
label { display: grid; gap: 9px; margin-bottom: 22px; color: var(--text-label); 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: #ffffff06; border: 1px solid var(--border-input); border-radius: 5px; outline: none; } input:focus, select:focus { border-color: var(--color-accent); }
.check { display: flex; align-items: center; grid-template-columns: auto 1fr; }.check input { width: auto; accent-color: var(--color-accent); }

/* Command output and actions (#194, #205) */
.output { display: flex; align-items: center; gap: 15px; padding: 16px; background: var(--surface-output); border-radius: 6px; }
.output code { flex: 1; color: var(--color-accent); font: .75rem/1.6 "DM Mono"; overflow-wrap: anywhere; }
.output-actions { display: flex; align-items: center; gap: 10px; }
.output button { color: var(--surface-base); background: var(--color-accent); border: 0; border-radius: 4px; padding: 8px 12px; cursor: pointer; font-weight: 600; white-space: nowrap; }
.output button.button-reset { color: var(--text-secondary); background: transparent; border: 1px solid var(--border-strong); font-weight: 500; }
.output button.button-reset:hover { color: var(--text-primary); border-color: var(--color-accent); }

/* Compact command output layout for 320-pixel screens (#194) */
@media (max-width: 480px) {
.output {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 12px;
}
.output code {
width: 100%;
font-size: .72rem;
line-height: 1.5;
word-break: break-all;
}
.output-actions {
display: flex;
gap: 8px;
width: 100%;
}
.output button {
flex: 1;
text-align: center;
padding: 10px;
}
}

/* Responsive footer layout with wrapping columns (#195) */
footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
gap: 32px;
padding: 48px 5vw;
border-top: 1px solid var(--border-subtle);
color: var(--text-muted);
font-size: .78rem;
}
.footer-col {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 150px;
}
.footer-brand {
color: var(--text-primary);
font-weight: 700;
font-size: .85rem;
}
.footer-col p {
margin: 0;
line-height: 1.5;
color: var(--text-body);
max-width: 280px;
}
.footer-heading {
color: var(--text-secondary);
font-size: .7rem;
font-family: "DM Mono", monospace;
text-transform: uppercase;
letter-spacing: .12em;
font-weight: 500;
}
.footer-links {
display: flex;
flex-direction: column;
gap: 6px;
}
.footer-links a:hover {
color: var(--color-accent);
}
.footer-meta {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--text-muted);
}

@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; } }
@media (max-width: 600px) {
footer {
flex-direction: column;
gap: 24px;
padding: 32px 22px;
}
}
@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; } }