Redesign CORS tester interface

This commit is contained in:
Meghdad
2026-06-21 04:29:32 +03:30
parent 6ff15abb8f
commit d4da94c414
2 changed files with 611 additions and 571 deletions

View File

@@ -623,34 +623,26 @@ export default function App() {
return ( return (
<main className="app-shell"> <main className="app-shell">
<section className="hero"> <header className="topbar">
<div> <div>
<p className="eyebrow">Static browser CORS diagnostics</p> <p className="eyebrow">Browser CORS tester</p>
<h1>CORS Test Lab</h1> <h1>CORS Test Lab</h1>
<p className="hero-copy">
Test real browser CORS behavior from this page origin, classify preflights, inspect readable responses,
and generate reports for API owners.
</p>
</div> </div>
<div className="origin-panel"> <div className="origin-badge">
<span>Testing origin</span> <span>Actual origin</span>
<strong>{getOriginLabel()}</strong> <strong>{getOriginLabel()}</strong>
</div> </div>
</section> </header>
<section className="dashboard"> <section className="workspace">
<div className="request-panel"> <div className="tester-card">
<div className="section-heading"> <div className="card-heading">
<div> <h2>Test an API endpoint</h2>
<p className="eyebrow">Request builder</p> <p>Run a real browser request from the origin shown above. The app cannot spoof the Origin header.</p>
<h2>Configure a test</h2>
</div>
<button className="primary" disabled={isRunning} onClick={() => void runTest()}>
{isRunning ? 'Running...' : 'Run test'}
</button>
</div> </div>
<label className="field field-full"> <div className="url-row">
<label className="field">
<span>API URL</span> <span>API URL</span>
<input <input
value={draft.url} value={draft.url}
@@ -659,8 +651,12 @@ export default function App() {
inputMode="url" inputMode="url"
/> />
</label> </label>
<button className="primary run-button" disabled={isRunning} onClick={() => void runTest()}>
{isRunning ? 'Running...' : 'Run test'}
</button>
</div>
<div className="control-grid"> <div className="request-grid">
<label className="field"> <label className="field">
<span>Method</span> <span>Method</span>
<select value={draft.method} onChange={(event) => patchDraft({ method: event.target.value as HttpMethod })}> <select value={draft.method} onChange={(event) => patchDraft({ method: event.target.value as HttpMethod })}>
@@ -706,7 +702,7 @@ export default function App() {
</label> </label>
</div> </div>
<div className="status-strip"> <div className="summary-strip">
<span className={preflightExpected ? 'pill warning' : 'pill ok'}> <span className={preflightExpected ? 'pill warning' : 'pill ok'}>
{preflightExpected ? 'Preflight expected' : 'Simple request shape'} {preflightExpected ? 'Preflight expected' : 'Simple request shape'}
</span> </span>
@@ -714,6 +710,29 @@ export default function App() {
<span className="pill">Credentials: {draft.credentials}</span> <span className="pill">Credentials: {draft.credentials}</span>
</div> </div>
<div className="quick-probes">
<div>
<h3>Quick probes</h3>
<p>Use the same URL with common CORS shapes.</p>
</div>
<div className="probe-list">
{scenarios.map((scenario) => (
<button
className="probe-chip"
key={scenario.id}
disabled={isRunning}
onClick={() => void runTest(scenario.overrides, scenario.title)}
title={scenario.description}
>
{scenario.title}
</button>
))}
</div>
</div>
<details className="details-panel">
<summary>Request details</summary>
<div className="details-body">
<div className="section-subhead"> <div className="section-subhead">
<h3>Headers</h3> <h3>Headers</h3>
<button className="ghost" onClick={() => patchDraft({ headers: [...draft.headers, emptyHeader()] })}> <button className="ghost" onClick={() => patchDraft({ headers: [...draft.headers, emptyHeader()] })}>
@@ -740,7 +759,7 @@ export default function App() {
))} ))}
</div> </div>
<div className="control-grid body-grid"> <div className="body-grid">
<label className="field"> <label className="field">
<span>Content type</span> <span>Content type</span>
<select value={draft.contentType} onChange={(event) => patchDraft({ contentType: event.target.value })}> <select value={draft.contentType} onChange={(event) => patchDraft({ contentType: event.target.value })}>
@@ -762,43 +781,20 @@ export default function App() {
</label> </label>
</div> </div>
</div> </div>
</details>
<aside className="side-panel"> <details className="details-panel">
<div className="section-heading compact"> <summary>Domain policy notes</summary>
<div> <div className="details-body">
<p className="eyebrow">Scenarios</p>
<h2>One-click probes</h2>
</div>
</div>
<div className="scenario-grid">
{scenarios.map((scenario) => (
<button
className="scenario-card"
key={scenario.id}
disabled={isRunning}
onClick={() => void runTest(scenario.overrides, scenario.title)}
>
<strong>{scenario.title}</strong>
<span>{scenario.description}</span>
</button>
))}
</div>
<div className="policy-panel">
<div className="section-heading compact">
<div>
<p className="eyebrow">Domain policy</p>
<h2>Expected origins</h2>
</div>
</div>
<div className="policy-origin"> <div className="policy-origin">
<span>Actual browser origin</span> <span>Actual browser origin</span>
<strong>{getOriginLabel()}</strong> <strong>{getOriginLabel()}</strong>
</div> </div>
<p className="policy-note"> <p className="policy-note">
This static page cannot spoof Origin. Use these lists to record expected allow/block rules; this run Add expected allowed or blocked origins for your report. This is not a spoofed test; to verify another
only proves behavior for the actual browser origin above. domain, run this app from that domain.
</p> </p>
<div className="policy-grid">
<label className="field policy-field"> <label className="field policy-field">
<span>Expected allowed origins</span> <span>Expected allowed origins</span>
<textarea <textarea
@@ -820,26 +816,27 @@ export default function App() {
/> />
</label> </label>
</div> </div>
</div>
</details>
<div className="saved-panel"> <details className="details-panel">
<div className="section-subhead"> <summary>Saved cases</summary>
<h3>Saved cases</h3> <div className="details-body">
<div className="button-pair">
<button className="ghost" disabled={!savedCases.length} onClick={exportCases}>
Export
</button>
<button className="ghost" onClick={() => fileInputRef.current?.click()}>
Import
</button>
</div>
</div>
<input ref={fileInputRef} hidden type="file" accept="application/json" onChange={(event) => void importCases(event)} />
<div className="save-row"> <div className="save-row">
<input value={caseName} onChange={(event) => setCaseName(event.target.value)} placeholder="Case name" /> <input value={caseName} onChange={(event) => setCaseName(event.target.value)} placeholder="Case name" />
<button className="secondary" onClick={saveCase}> <button className="secondary" onClick={saveCase}>
Save Save
</button> </button>
</div> </div>
<div className="saved-actions">
<button className="ghost" disabled={!savedCases.length} onClick={exportCases}>
Export JSON
</button>
<button className="ghost" onClick={() => fileInputRef.current?.click()}>
Import JSON
</button>
</div>
<input ref={fileInputRef} hidden type="file" accept="application/json" onChange={(event) => void importCases(event)} />
<div className="saved-list"> <div className="saved-list">
{savedCases.length === 0 ? ( {savedCases.length === 0 ? (
<p className="muted">No saved cases yet.</p> <p className="muted">No saved cases yet.</p>
@@ -864,15 +861,14 @@ export default function App() {
)} )}
</div> </div>
</div> </div>
</aside> </details>
</section> </div>
<section className="results-layout">
<div className="result-panel"> <div className="result-panel">
<div className="section-heading"> <div className="card-heading result-heading">
<div> <div>
<p className="eyebrow">Latest result</p> <h2>Result</h2>
<h2>{latestResult ? latestResult.label : 'No test run yet'}</h2> <p>{latestResult ? latestResult.label : 'Run a test to see browser evidence and diagnostics.'}</p>
</div> </div>
{latestResult ? ( {latestResult ? (
<button className="secondary" onClick={() => void copyReport(latestResult)}> <button className="secondary" onClick={() => void copyReport(latestResult)}>
@@ -892,13 +888,16 @@ export default function App() {
<span className={`big-status ${latestResult.status}`}>{latestResult.status}</span> <span className={`big-status ${latestResult.status}`}>{latestResult.status}</span>
<div> <div>
<strong>{latestResult.browserResult}</strong> <strong>{latestResult.browserResult}</strong>
<span> <span>{latestResult.durationMs}ms</span>
{latestResult.durationMs}ms
{latestResult.statusCode ? `, HTTP ${latestResult.statusCode} ${latestResult.statusText ?? ''}` : ''}
</span>
</div> </div>
</div> </div>
<div className="result-meta">
<span>HTTP: {latestResult.statusCode ? `${latestResult.statusCode} ${latestResult.statusText ?? ''}` : 'not readable'}</span>
<span>Type: {latestResult.responseType ?? 'not available'}</span>
<span>Origin: {getOriginLabel()}</span>
</div>
{latestResult.error ? <div className="alert danger">{latestResult.error}</div> : null} {latestResult.error ? <div className="alert danger">{latestResult.error}</div> : null}
{latestResult.warnings.map((warning) => ( {latestResult.warnings.map((warning) => (
<div className="alert warning" key={warning}> <div className="alert warning" key={warning}>
@@ -922,15 +921,9 @@ export default function App() {
</div> </div>
</> </>
)} )}
</div>
<div className="analysis-panel"> <div className="analysis-block">
<div className="section-heading compact"> <h3>Diagnostics</h3>
<div>
<p className="eyebrow">Analysis</p>
<h2>What it means</h2>
</div>
</div>
{latestResult ? ( {latestResult ? (
<ul className="diagnostics-list"> <ul className="diagnostics-list">
{latestResult.diagnostics.map((diagnostic) => ( {latestResult.diagnostics.map((diagnostic) => (
@@ -940,6 +933,7 @@ export default function App() {
) : ( ) : (
<p className="muted">Diagnostics appear after a run.</p> <p className="muted">Diagnostics appear after a run.</p>
)} )}
</div>
<div className="history"> <div className="history">
<h3>Run history</h3> <h3>Run history</h3>

View File

@@ -2,8 +2,8 @@
color-scheme: light; color-scheme: light;
font-family: font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033; color: #182230;
background: #eef4f8; background: #f4f6f8;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -14,12 +14,10 @@
} }
body { body {
margin: 0;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background: margin: 0;
radial-gradient(circle at top left, rgba(37, 99, 235, 0.18), transparent 30rem), background: #f4f6f8;
linear-gradient(135deg, #f8fbff 0%, #eef4f8 44%, #e8eef5 100%);
} }
button, button,
@@ -38,29 +36,6 @@ button:disabled {
opacity: 0.55; opacity: 0.55;
} }
.app-shell {
width: min(1480px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 48px;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
gap: 24px;
align-items: end;
margin-bottom: 24px;
}
.eyebrow {
margin: 0 0 8px;
color: #2563eb;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1, h1,
h2, h2,
h3, h3,
@@ -68,142 +43,162 @@ p {
margin-top: 0; margin-top: 0;
} }
h1 { .app-shell {
margin-bottom: 12px; width: min(1280px, calc(100% - 32px));
color: #0f172a; margin: 0 auto;
font-size: clamp(2.8rem, 7vw, 6.5rem); padding: 24px 0 48px;
line-height: 0.95;
} }
h2 { .topbar {
margin-bottom: 0;
color: #111827;
font-size: 1.35rem;
}
h3 {
margin-bottom: 10px;
color: #1f2937;
font-size: 0.98rem;
}
.hero-copy {
max-width: 820px;
margin-bottom: 0;
color: #526173;
font-size: 1.08rem;
line-height: 1.65;
}
.origin-panel {
padding: 18px;
border: 1px solid rgba(37, 99, 235, 0.18);
border-radius: 8px;
background: rgba(255, 255, 255, 0.76);
box-shadow: 0 18px 50px rgba(30, 41, 59, 0.08);
}
.origin-panel span,
.field span {
display: block;
margin-bottom: 7px;
color: #64748b;
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
}
.origin-panel strong {
display: block;
overflow-wrap: anywhere;
color: #0f172a;
}
.dashboard,
.results-layout {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(360px, 0.8fr);
gap: 20px;
align-items: start;
}
.results-layout {
margin-top: 20px;
}
.request-panel,
.side-panel,
.result-panel,
.analysis-panel {
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 8px;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
}
.request-panel,
.result-panel,
.analysis-panel {
padding: 22px;
}
.side-panel {
padding: 18px;
}
.section-heading,
.section-subhead {
display: flex; display: flex;
gap: 16px; gap: 20px;
align-items: center; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
margin-bottom: 18px; margin-bottom: 18px;
} }
.section-heading.compact { .eyebrow {
margin-bottom: 14px; margin-bottom: 6px;
color: #2563eb;
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
} }
.section-subhead { h1 {
margin: 18px 0 10px; margin-bottom: 0;
color: #101828;
font-size: 2.25rem;
line-height: 1;
} }
.section-subhead h3 { h2 {
margin: 0; margin-bottom: 6px;
color: #101828;
font-size: 1.18rem;
line-height: 1.25;
}
h3 {
margin-bottom: 8px;
color: #182230;
font-size: 0.96rem;
line-height: 1.35;
}
.origin-badge {
max-width: 520px;
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 10px 12px;
background: #ffffff;
}
.origin-badge span,
.field span,
.policy-origin span {
display: block;
margin-bottom: 5px;
color: #667085;
font-size: 0.72rem;
font-weight: 800;
text-transform: uppercase;
}
.origin-badge strong,
.policy-origin strong {
display: block;
color: #101828;
font-size: 0.9rem;
overflow-wrap: anywhere;
}
.workspace {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(420px, 1.05fr);
gap: 18px;
align-items: start;
}
.tester-card,
.result-panel {
border: 1px solid #d0d5dd;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 12px 30px rgba(16, 24, 40, 0.06);
}
.tester-card {
padding: 18px;
}
.result-panel {
padding: 18px;
}
.card-heading {
margin-bottom: 16px;
}
.card-heading p,
.quick-probes p,
.policy-note,
.muted {
margin-bottom: 0;
color: #667085;
font-size: 0.9rem;
line-height: 1.55;
}
.result-heading {
display: flex;
gap: 12px;
align-items: flex-start;
justify-content: space-between;
}
.url-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: end;
}
.request-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
} }
.field { .field {
display: block; display: block;
} }
.field-full {
margin-bottom: 16px;
}
input, input,
select, select,
textarea { textarea {
width: 100%; width: 100%;
border: 1px solid #d4dce7; border: 1px solid #d0d5dd;
border-radius: 8px; border-radius: 8px;
background: #ffffff; background: #ffffff;
color: #172033; color: #182230;
outline: none; outline: none;
transition: transition:
border-color 160ms ease, border-color 160ms ease,
box-shadow 160ms ease, box-shadow 160ms ease;
background 160ms ease;
} }
input, input,
select { select {
height: 42px; height: 42px;
padding: 0 12px; padding: 0 11px;
} }
textarea { textarea {
min-height: 156px; min-height: 130px;
padding: 12px; padding: 10px 11px;
resize: vertical; resize: vertical;
line-height: 1.5; line-height: 1.5;
} }
@@ -212,22 +207,173 @@ input:focus,
select:focus, select:focus,
textarea:focus { textarea:focus {
border-color: #2563eb; border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.14); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
} }
.control-grid { .primary,
.secondary,
.ghost,
.icon-button,
.probe-chip {
min-height: 40px;
border-radius: 8px;
font-weight: 800;
}
.primary {
border: 1px solid #1d4ed8;
background: #2563eb;
color: #ffffff;
}
.primary:hover {
background: #1d4ed8;
}
.run-button {
min-width: 132px;
padding: 0 18px;
}
.secondary {
border: 1px solid #bfdbfe;
background: #eff6ff;
color: #1d4ed8;
padding: 0 13px;
}
.ghost {
border: 1px solid #d0d5dd;
background: #ffffff;
color: #344054;
padding: 0 13px;
}
.icon-button {
width: 42px;
border: 1px solid #d0d5dd;
background: #ffffff;
color: #667085;
}
.summary-strip,
.probe-list,
.saved-actions,
.result-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.summary-strip {
margin-top: 12px;
}
.pill {
display: inline-flex;
min-height: 28px;
align-items: center;
border: 1px solid #d0d5dd;
border-radius: 999px;
padding: 5px 10px;
background: #f9fafb;
color: #475467;
font-size: 0.8rem;
font-weight: 800;
}
.pill.ok {
border-color: #bbf7d0;
background: #f0fdf4;
color: #15803d;
}
.pill.warning {
border-color: #fed7aa;
background: #fff7ed;
color: #b45309;
}
.quick-probes {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px;
gap: 12px;
}
.body-grid {
grid-template-columns: minmax(180px, 0.32fr) minmax(0, 1fr);
margin-top: 16px; margin-top: 16px;
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 12px;
background: #f9fafb;
} }
.field-body { .quick-probes h3 {
grid-row: span 2; margin-bottom: 2px;
}
.probe-chip {
border: 1px solid #d0d5dd;
background: #ffffff;
color: #344054;
padding: 0 11px;
}
.probe-chip:hover,
.ghost:hover,
.saved-item:hover,
.history-row:hover {
border-color: #98a2b3;
background: #f9fafb;
}
.details-panel {
margin-top: 12px;
border: 1px solid #e4e7ec;
border-radius: 8px;
background: #ffffff;
}
.details-panel summary {
display: flex;
align-items: center;
min-height: 46px;
padding: 0 13px;
color: #182230;
font-weight: 850;
list-style: none;
cursor: pointer;
}
.details-panel summary::-webkit-details-marker {
display: none;
}
.details-panel summary::after {
margin-left: auto;
color: #667085;
content: "+";
font-size: 1.1rem;
}
.details-panel[open] summary {
border-bottom: 1px solid #e4e7ec;
}
.details-panel[open] summary::after {
content: "-";
}
.details-body {
padding: 13px;
}
.section-subhead {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.section-subhead h3 {
margin-bottom: 0;
} }
.headers-list { .headers-list {
@@ -235,184 +381,56 @@ textarea:focus {
gap: 8px; gap: 8px;
} }
.header-row, .header-row {
.save-row {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 42px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 42px;
gap: 8px; gap: 8px;
} }
.save-row { .body-grid,
grid-template-columns: minmax(0, 1fr) auto; .policy-grid,
} .detail-grid {
.status-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.pill {
display: inline-flex;
min-height: 30px;
align-items: center;
border: 1px solid #dbe3ef;
border-radius: 999px;
padding: 6px 10px;
background: #f8fafc;
color: #475569;
font-size: 0.82rem;
font-weight: 800;
}
.pill.ok {
border-color: rgba(22, 163, 74, 0.25);
background: #edfdf3;
color: #15803d;
}
.pill.warning {
border-color: rgba(217, 119, 6, 0.28);
background: #fff7ed;
color: #b45309;
}
.primary,
.secondary,
.ghost,
.icon-button {
min-height: 40px;
border: 0;
border-radius: 8px;
padding: 0 14px;
font-weight: 800;
}
.primary {
background: #2563eb;
color: #ffffff;
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.28);
}
.secondary {
border: 1px solid #bfdbfe;
background: #eff6ff;
color: #1d4ed8;
}
.ghost {
border: 1px solid #d8e1ed;
background: #ffffff;
color: #334155;
}
.icon-button {
width: 42px;
padding: 0;
border: 1px solid #d8e1ed;
background: #ffffff;
color: #64748b;
}
.button-pair {
display: flex;
gap: 8px;
}
.scenario-grid {
display: grid; display: grid;
gap: 10px; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
} }
.scenario-card { .body-grid {
width: 100%;
border: 1px solid #d9e2ee;
border-radius: 8px;
padding: 14px;
background: #ffffff;
color: #172033;
text-align: left;
}
.scenario-card:hover,
.saved-item:hover,
.history-row:hover {
border-color: #93c5fd;
background: #f8fbff;
}
.scenario-card strong,
.scenario-card span,
.saved-item strong,
.saved-item span {
display: block;
}
.scenario-card span,
.saved-item span,
.history-row span,
.policy-note,
.muted {
color: #64748b;
font-size: 0.9rem;
}
.policy-panel {
margin-top: 20px;
border-top: 1px solid #e2e8f0;
padding-top: 18px;
}
.policy-origin {
border: 1px solid #dbeafe;
border-radius: 8px;
padding: 12px;
background: #eff6ff;
}
.policy-origin span,
.policy-origin strong {
display: block;
}
.policy-origin span {
margin-bottom: 4px;
color: #2563eb;
font-size: 0.76rem;
font-weight: 900;
text-transform: uppercase;
}
.policy-origin strong {
overflow-wrap: anywhere;
color: #1e3a8a;
}
.policy-note {
margin: 10px 0 14px;
line-height: 1.5;
}
.policy-field {
margin-top: 12px; margin-top: 12px;
} }
.origin-textarea { .origin-textarea {
min-height: 94px; min-height: 96px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.84rem; font-size: 0.84rem;
} }
.saved-panel { .policy-origin {
margin-top: 20px; border: 1px solid #bfdbfe;
border-radius: 8px;
padding: 10px;
background: #eff6ff;
}
.policy-note {
margin: 10px 0 12px;
}
.save-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
}
.saved-actions {
margin-top: 8px;
} }
.saved-list { .saved-list {
display: grid; display: grid;
gap: 8px; gap: 8px;
margin-top: 12px; margin-top: 12px;
max-height: 360px; max-height: 280px;
overflow: auto; overflow: auto;
} }
@@ -420,10 +438,10 @@ textarea:focus {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 42px; grid-template-columns: minmax(0, 1fr) 42px;
gap: 8px; gap: 8px;
border: 1px solid #d9e2ee; border: 1px solid #e4e7ec;
border-radius: 8px; border-radius: 8px;
background: #ffffff;
padding: 8px; padding: 8px;
background: #ffffff;
} }
.saved-item > button:first-child { .saved-item > button:first-child {
@@ -433,15 +451,41 @@ textarea:focus {
text-align: left; text-align: left;
} }
.saved-item strong,
.saved-item span {
display: block;
}
.saved-item span,
.history-row span {
color: #667085;
font-size: 0.86rem;
}
.empty-state {
display: grid;
min-height: 220px;
place-content: center;
border: 1px dashed #cfd4dc;
border-radius: 8px;
background: #f9fafb;
color: #667085;
text-align: center;
}
.empty-state strong {
margin-bottom: 4px;
color: #182230;
}
.result-summary { .result-summary {
display: flex; display: flex;
gap: 14px; gap: 12px;
align-items: center; align-items: center;
margin-bottom: 16px; border: 1px solid #e4e7ec;
border: 1px solid #dbe3ef;
border-radius: 8px; border-radius: 8px;
padding: 14px; padding: 12px;
background: #f8fafc; background: #f9fafb;
} }
.result-summary strong, .result-summary strong,
@@ -451,9 +495,9 @@ textarea:focus {
.big-status { .big-status {
border-radius: 8px; border-radius: 8px;
padding: 12px 14px; padding: 10px 12px;
color: #ffffff; color: #ffffff;
font-size: 0.9rem; font-size: 0.82rem;
font-weight: 900; font-weight: 900;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -473,11 +517,26 @@ textarea:focus {
background: #7c3aed; background: #7c3aed;
} }
.result-meta {
margin: 10px 0 12px;
}
.result-meta span {
border: 1px solid #e4e7ec;
border-radius: 999px;
padding: 5px 9px;
background: #ffffff;
color: #475467;
font-size: 0.82rem;
font-weight: 750;
overflow-wrap: anywhere;
}
.alert { .alert {
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 10px 12px;
font-weight: 700; font-weight: 750;
} }
.alert.danger { .alert.danger {
@@ -492,19 +551,13 @@ textarea:focus {
color: #b45309; color: #b45309;
} }
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
pre { pre {
max-height: 360px; max-height: 300px;
overflow: auto; overflow: auto;
border: 1px solid #dbe3ef; border: 1px solid #e4e7ec;
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 11px;
background: #0f172a; background: #101828;
color: #dbeafe; color: #dbeafe;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.82rem; font-size: 0.82rem;
@@ -513,36 +566,41 @@ pre {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.analysis-block {
margin-top: 18px;
}
.diagnostics-list { .diagnostics-list {
display: grid; display: grid;
gap: 10px; gap: 8px;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
.diagnostics-list li { .diagnostics-list li {
border: 1px solid #dbe3ef; border: 1px solid #e4e7ec;
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 10px 12px;
background: #f8fafc; background: #ffffff;
color: #334155; color: #344054;
line-height: 1.45;
} }
.history { .history {
margin-top: 22px; margin-top: 18px;
} }
.history-row { .history-row {
display: grid; display: grid;
width: 100%; width: 100%;
grid-template-columns: 10px minmax(0, 1fr) auto; grid-template-columns: 10px minmax(0, 1fr) auto;
gap: 10px; gap: 9px;
align-items: center; align-items: center;
border: 1px solid #dbe3ef; border: 1px solid #e4e7ec;
border-radius: 8px; border-radius: 8px;
margin-top: 8px; margin-top: 8px;
padding: 10px; padding: 9px 10px;
background: #ffffff; background: #ffffff;
color: inherit; color: inherit;
text-align: left; text-align: left;
@@ -554,77 +612,65 @@ pre {
border-radius: 999px; border-radius: 999px;
} }
.empty-state { @media (max-width: 980px) {
display: grid; .topbar,
min-height: 220px; .workspace {
place-content: center;
border: 1px dashed #bfcbda;
border-radius: 8px;
background: #f8fafc;
color: #64748b;
text-align: center;
}
.empty-state strong {
color: #172033;
}
@media (max-width: 1120px) {
.hero,
.dashboard,
.results-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.side-panel { .topbar {
order: 2; display: grid;
align-items: start;
}
.workspace {
display: grid;
}
.request-grid,
.detail-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 760px) { @media (max-width: 700px) {
.app-shell { .app-shell {
width: min(100% - 20px, 1480px); width: min(100% - 20px, 1280px);
padding-top: 18px; padding-top: 16px;
} }
h1 { h1 {
font-size: 3.1rem; font-size: 1.9rem;
} }
.request-panel, .tester-card,
.side-panel, .result-panel {
.result-panel,
.analysis-panel {
padding: 14px; padding: 14px;
} }
.section-heading, .url-row,
.section-subhead, .request-grid,
.result-summary { .header-row,
.body-grid,
.policy-grid,
.detail-grid,
.save-row,
.history-row {
grid-template-columns: 1fr;
}
.result-heading,
.section-subhead {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
} }
.control-grid, .run-button,
.body-grid,
.header-row,
.detail-grid {
grid-template-columns: 1fr;
}
.header-row {
grid-template-columns: 1fr;
}
.icon-button { .icon-button {
width: 100%; width: 100%;
} }
.history-row {
grid-template-columns: 10px minmax(0, 1fr);
}
.history-row span:last-child { .history-row span:last-child {
grid-column: 2; grid-column: 1;
} }
} }