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,44 +623,40 @@ export default function App() {
return (
<main className="app-shell">
<section className="hero">
<header className="topbar">
<div>
<p className="eyebrow">Static browser CORS diagnostics</p>
<p className="eyebrow">Browser CORS tester</p>
<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 className="origin-panel">
<span>Testing origin</span>
<div className="origin-badge">
<span>Actual origin</span>
<strong>{getOriginLabel()}</strong>
</div>
</section>
</header>
<section className="dashboard">
<div className="request-panel">
<div className="section-heading">
<div>
<p className="eyebrow">Request builder</p>
<h2>Configure a test</h2>
</div>
<button className="primary" disabled={isRunning} onClick={() => void runTest()}>
<section className="workspace">
<div className="tester-card">
<div className="card-heading">
<h2>Test an API endpoint</h2>
<p>Run a real browser request from the origin shown above. The app cannot spoof the Origin header.</p>
</div>
<div className="url-row">
<label className="field">
<span>API URL</span>
<input
value={draft.url}
onChange={(event) => patchDraft({ url: event.target.value })}
placeholder="https://api.example.com/v1/resource"
inputMode="url"
/>
</label>
<button className="primary run-button" disabled={isRunning} onClick={() => void runTest()}>
{isRunning ? 'Running...' : 'Run test'}
</button>
</div>
<label className="field field-full">
<span>API URL</span>
<input
value={draft.url}
onChange={(event) => patchDraft({ url: event.target.value })}
placeholder="https://api.example.com/v1/resource"
inputMode="url"
/>
</label>
<div className="control-grid">
<div className="request-grid">
<label className="field">
<span>Method</span>
<select value={draft.method} onChange={(event) => patchDraft({ method: event.target.value as HttpMethod })}>
@@ -706,7 +702,7 @@ export default function App() {
</label>
</div>
<div className="status-strip">
<div className="summary-strip">
<span className={preflightExpected ? 'pill warning' : 'pill ok'}>
{preflightExpected ? 'Preflight expected' : 'Simple request shape'}
</span>
@@ -714,165 +710,165 @@ export default function App() {
<span className="pill">Credentials: {draft.credentials}</span>
</div>
<div className="section-subhead">
<h3>Headers</h3>
<button className="ghost" onClick={() => patchDraft({ headers: [...draft.headers, emptyHeader()] })}>
Add header
</button>
</div>
<div className="headers-list">
{draft.headers.map((header) => (
<div className="header-row" key={header.id}>
<input
value={header.name}
onChange={(event) => updateHeader(header.id, 'name', event.target.value)}
placeholder="Header name"
/>
<input
value={header.value}
onChange={(event) => updateHeader(header.id, 'value', event.target.value)}
placeholder="Value"
/>
<button className="icon-button" aria-label="Remove header" onClick={() => removeHeader(header.id)}>
x
</button>
</div>
))}
</div>
<div className="control-grid body-grid">
<label className="field">
<span>Content type</span>
<select value={draft.contentType} onChange={(event) => patchDraft({ contentType: event.target.value })}>
{CONTENT_TYPES.map((contentType) => (
<option key={contentType.value} value={contentType.value}>
{contentType.label}
</option>
))}
</select>
</label>
<label className="field field-body">
<span>Body</span>
<textarea
value={draft.body}
onChange={(event) => patchDraft({ body: event.target.value })}
placeholder='{"example": true}'
spellCheck={false}
/>
</label>
</div>
</div>
<aside className="side-panel">
<div className="section-heading compact">
<div className="quick-probes">
<div>
<p className="eyebrow">Scenarios</p>
<h2>One-click probes</h2>
<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>
<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">
<span>Actual browser origin</span>
<strong>{getOriginLabel()}</strong>
</div>
<p className="policy-note">
This static page cannot spoof Origin. Use these lists to record expected allow/block rules; this run
only proves behavior for the actual browser origin above.
</p>
<label className="field policy-field">
<span>Expected allowed origins</span>
<textarea
className="origin-textarea"
value={originLines(draft.allowedOrigins)}
onChange={(event) => patchDraft({ allowedOrigins: parseOriginLines(event.target.value) })}
placeholder={'https://app.example.com\nhttps://admin.example.com'}
spellCheck={false}
/>
</label>
<label className="field policy-field">
<span>Expected blocked origins</span>
<textarea
className="origin-textarea"
value={originLines(draft.blockedOrigins)}
onChange={(event) => patchDraft({ blockedOrigins: parseOriginLines(event.target.value) })}
placeholder={'https://unknown.example.com\nhttps://staging.example.net'}
spellCheck={false}
/>
</label>
</div>
<div className="saved-panel">
<div className="section-subhead">
<h3>Saved cases</h3>
<div className="button-pair">
<button className="ghost" disabled={!savedCases.length} onClick={exportCases}>
Export
</button>
<button className="ghost" onClick={() => fileInputRef.current?.click()}>
Import
<details className="details-panel">
<summary>Request details</summary>
<div className="details-body">
<div className="section-subhead">
<h3>Headers</h3>
<button className="ghost" onClick={() => patchDraft({ headers: [...draft.headers, emptyHeader()] })}>
Add header
</button>
</div>
</div>
<input ref={fileInputRef} hidden type="file" accept="application/json" onChange={(event) => void importCases(event)} />
<div className="save-row">
<input value={caseName} onChange={(event) => setCaseName(event.target.value)} placeholder="Case name" />
<button className="secondary" onClick={saveCase}>
Save
</button>
</div>
<div className="saved-list">
{savedCases.length === 0 ? (
<p className="muted">No saved cases yet.</p>
) : (
savedCases.map((saved) => (
<div className="saved-item" key={saved.id}>
<button onClick={() => loadCase(saved)}>
<strong>{saved.name}</strong>
<span>
{saved.method} {saved.url || 'No URL'}
</span>
</button>
<button
className="icon-button"
aria-label={`Delete ${saved.name}`}
onClick={() => setSavedCases((current) => current.filter((item) => item.id !== saved.id))}
>
<div className="headers-list">
{draft.headers.map((header) => (
<div className="header-row" key={header.id}>
<input
value={header.name}
onChange={(event) => updateHeader(header.id, 'name', event.target.value)}
placeholder="Header name"
/>
<input
value={header.value}
onChange={(event) => updateHeader(header.id, 'value', event.target.value)}
placeholder="Value"
/>
<button className="icon-button" aria-label="Remove header" onClick={() => removeHeader(header.id)}>
x
</button>
</div>
))
)}
</div>
</div>
</aside>
</section>
))}
</div>
<div className="body-grid">
<label className="field">
<span>Content type</span>
<select value={draft.contentType} onChange={(event) => patchDraft({ contentType: event.target.value })}>
{CONTENT_TYPES.map((contentType) => (
<option key={contentType.value} value={contentType.value}>
{contentType.label}
</option>
))}
</select>
</label>
<label className="field field-body">
<span>Body</span>
<textarea
value={draft.body}
onChange={(event) => patchDraft({ body: event.target.value })}
placeholder='{"example": true}'
spellCheck={false}
/>
</label>
</div>
</div>
</details>
<details className="details-panel">
<summary>Domain policy notes</summary>
<div className="details-body">
<div className="policy-origin">
<span>Actual browser origin</span>
<strong>{getOriginLabel()}</strong>
</div>
<p className="policy-note">
Add expected allowed or blocked origins for your report. This is not a spoofed test; to verify another
domain, run this app from that domain.
</p>
<div className="policy-grid">
<label className="field policy-field">
<span>Expected allowed origins</span>
<textarea
className="origin-textarea"
value={originLines(draft.allowedOrigins)}
onChange={(event) => patchDraft({ allowedOrigins: parseOriginLines(event.target.value) })}
placeholder={'https://app.example.com\nhttps://admin.example.com'}
spellCheck={false}
/>
</label>
<label className="field policy-field">
<span>Expected blocked origins</span>
<textarea
className="origin-textarea"
value={originLines(draft.blockedOrigins)}
onChange={(event) => patchDraft({ blockedOrigins: parseOriginLines(event.target.value) })}
placeholder={'https://unknown.example.com\nhttps://staging.example.net'}
spellCheck={false}
/>
</label>
</div>
</div>
</details>
<details className="details-panel">
<summary>Saved cases</summary>
<div className="details-body">
<div className="save-row">
<input value={caseName} onChange={(event) => setCaseName(event.target.value)} placeholder="Case name" />
<button className="secondary" onClick={saveCase}>
Save
</button>
</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">
{savedCases.length === 0 ? (
<p className="muted">No saved cases yet.</p>
) : (
savedCases.map((saved) => (
<div className="saved-item" key={saved.id}>
<button onClick={() => loadCase(saved)}>
<strong>{saved.name}</strong>
<span>
{saved.method} {saved.url || 'No URL'}
</span>
</button>
<button
className="icon-button"
aria-label={`Delete ${saved.name}`}
onClick={() => setSavedCases((current) => current.filter((item) => item.id !== saved.id))}
>
x
</button>
</div>
))
)}
</div>
</div>
</details>
</div>
<section className="results-layout">
<div className="result-panel">
<div className="section-heading">
<div className="card-heading result-heading">
<div>
<p className="eyebrow">Latest result</p>
<h2>{latestResult ? latestResult.label : 'No test run yet'}</h2>
<h2>Result</h2>
<p>{latestResult ? latestResult.label : 'Run a test to see browser evidence and diagnostics.'}</p>
</div>
{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>
<div>
<strong>{latestResult.browserResult}</strong>
<span>
{latestResult.durationMs}ms
{latestResult.statusCode ? `, HTTP ${latestResult.statusCode} ${latestResult.statusText ?? ''}` : ''}
</span>
<span>{latestResult.durationMs}ms</span>
</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.warnings.map((warning) => (
<div className="alert warning" key={warning}>
@@ -922,24 +921,19 @@ export default function App() {
</div>
</>
)}
</div>
<div className="analysis-panel">
<div className="section-heading compact">
<div>
<p className="eyebrow">Analysis</p>
<h2>What it means</h2>
</div>
<div className="analysis-block">
<h3>Diagnostics</h3>
{latestResult ? (
<ul className="diagnostics-list">
{latestResult.diagnostics.map((diagnostic) => (
<li key={diagnostic}>{diagnostic}</li>
))}
</ul>
) : (
<p className="muted">Diagnostics appear after a run.</p>
)}
</div>
{latestResult ? (
<ul className="diagnostics-list">
{latestResult.diagnostics.map((diagnostic) => (
<li key={diagnostic}>{diagnostic}</li>
))}
</ul>
) : (
<p className="muted">Diagnostics appear after a run.</p>
)}
<div className="history">
<h3>Run history</h3>

View File

@@ -2,8 +2,8 @@
color-scheme: light;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef4f8;
color: #182230;
background: #f4f6f8;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@@ -14,12 +14,10 @@
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.18), transparent 30rem),
linear-gradient(135deg, #f8fbff 0%, #eef4f8 44%, #e8eef5 100%);
margin: 0;
background: #f4f6f8;
}
button,
@@ -38,29 +36,6 @@ button:disabled {
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,
h2,
h3,
@@ -68,142 +43,162 @@ p {
margin-top: 0;
}
h1 {
margin-bottom: 12px;
color: #0f172a;
font-size: clamp(2.8rem, 7vw, 6.5rem);
line-height: 0.95;
.app-shell {
width: min(1280px, calc(100% - 32px));
margin: 0 auto;
padding: 24px 0 48px;
}
h2 {
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 {
.topbar {
display: flex;
gap: 16px;
align-items: center;
gap: 20px;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 18px;
}
.section-heading.compact {
margin-bottom: 14px;
.eyebrow {
margin-bottom: 6px;
color: #2563eb;
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.section-subhead {
margin: 18px 0 10px;
h1 {
margin-bottom: 0;
color: #101828;
font-size: 2.25rem;
line-height: 1;
}
.section-subhead h3 {
margin: 0;
h2 {
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 {
display: block;
}
.field-full {
margin-bottom: 16px;
}
input,
select,
textarea {
width: 100%;
border: 1px solid #d4dce7;
border: 1px solid #d0d5dd;
border-radius: 8px;
background: #ffffff;
color: #172033;
color: #182230;
outline: none;
transition:
border-color 160ms ease,
box-shadow 160ms ease,
background 160ms ease;
box-shadow 160ms ease;
}
input,
select {
height: 42px;
padding: 0 12px;
padding: 0 11px;
}
textarea {
min-height: 156px;
padding: 12px;
min-height: 130px;
padding: 10px 11px;
resize: vertical;
line-height: 1.5;
}
@@ -212,22 +207,173 @@ input:focus,
select:focus,
textarea:focus {
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;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.body-grid {
grid-template-columns: minmax(180px, 0.32fr) minmax(0, 1fr);
gap: 10px;
margin-top: 16px;
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 12px;
background: #f9fafb;
}
.field-body {
grid-row: span 2;
.quick-probes h3 {
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 {
@@ -235,184 +381,56 @@ textarea:focus {
gap: 8px;
}
.header-row,
.save-row {
.header-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 42px;
gap: 8px;
}
.save-row {
grid-template-columns: minmax(0, 1fr) auto;
}
.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 {
.body-grid,
.policy-grid,
.detail-grid {
display: grid;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.scenario-card {
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 {
.body-grid {
margin-top: 12px;
}
.origin-textarea {
min-height: 94px;
min-height: 96px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.84rem;
}
.saved-panel {
margin-top: 20px;
.policy-origin {
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 {
display: grid;
gap: 8px;
margin-top: 12px;
max-height: 360px;
max-height: 280px;
overflow: auto;
}
@@ -420,10 +438,10 @@ textarea:focus {
display: grid;
grid-template-columns: minmax(0, 1fr) 42px;
gap: 8px;
border: 1px solid #d9e2ee;
border: 1px solid #e4e7ec;
border-radius: 8px;
background: #ffffff;
padding: 8px;
background: #ffffff;
}
.saved-item > button:first-child {
@@ -433,15 +451,41 @@ textarea:focus {
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 {
display: flex;
gap: 14px;
gap: 12px;
align-items: center;
margin-bottom: 16px;
border: 1px solid #dbe3ef;
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 14px;
background: #f8fafc;
padding: 12px;
background: #f9fafb;
}
.result-summary strong,
@@ -451,9 +495,9 @@ textarea:focus {
.big-status {
border-radius: 8px;
padding: 12px 14px;
padding: 10px 12px;
color: #ffffff;
font-size: 0.9rem;
font-size: 0.82rem;
font-weight: 900;
text-transform: uppercase;
}
@@ -473,11 +517,26 @@ textarea:focus {
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 {
margin-bottom: 10px;
border-radius: 8px;
padding: 12px;
font-weight: 700;
padding: 10px 12px;
font-weight: 750;
}
.alert.danger {
@@ -492,19 +551,13 @@ textarea:focus {
color: #b45309;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
pre {
max-height: 360px;
max-height: 300px;
overflow: auto;
border: 1px solid #dbe3ef;
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 12px;
background: #0f172a;
padding: 11px;
background: #101828;
color: #dbeafe;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.82rem;
@@ -513,36 +566,41 @@ pre {
overflow-wrap: anywhere;
}
.analysis-block {
margin-top: 18px;
}
.diagnostics-list {
display: grid;
gap: 10px;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.diagnostics-list li {
border: 1px solid #dbe3ef;
border: 1px solid #e4e7ec;
border-radius: 8px;
padding: 12px;
background: #f8fafc;
color: #334155;
padding: 10px 12px;
background: #ffffff;
color: #344054;
line-height: 1.45;
}
.history {
margin-top: 22px;
margin-top: 18px;
}
.history-row {
display: grid;
width: 100%;
grid-template-columns: 10px minmax(0, 1fr) auto;
gap: 10px;
gap: 9px;
align-items: center;
border: 1px solid #dbe3ef;
border: 1px solid #e4e7ec;
border-radius: 8px;
margin-top: 8px;
padding: 10px;
padding: 9px 10px;
background: #ffffff;
color: inherit;
text-align: left;
@@ -554,77 +612,65 @@ pre {
border-radius: 999px;
}
.empty-state {
display: grid;
min-height: 220px;
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 {
@media (max-width: 980px) {
.topbar,
.workspace {
grid-template-columns: 1fr;
}
.side-panel {
order: 2;
.topbar {
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 {
width: min(100% - 20px, 1480px);
padding-top: 18px;
width: min(100% - 20px, 1280px);
padding-top: 16px;
}
h1 {
font-size: 3.1rem;
font-size: 1.9rem;
}
.request-panel,
.side-panel,
.result-panel,
.analysis-panel {
.tester-card,
.result-panel {
padding: 14px;
}
.section-heading,
.section-subhead,
.result-summary {
.url-row,
.request-grid,
.header-row,
.body-grid,
.policy-grid,
.detail-grid,
.save-row,
.history-row {
grid-template-columns: 1fr;
}
.result-heading,
.section-subhead {
align-items: stretch;
flex-direction: column;
}
.control-grid,
.body-grid,
.header-row,
.detail-grid {
grid-template-columns: 1fr;
}
.header-row {
grid-template-columns: 1fr;
}
.run-button,
.icon-button {
width: 100%;
}
.history-row {
grid-template-columns: 10px minmax(0, 1fr);
}
.history-row span:last-child {
grid-column: 2;
grid-column: 1;
}
}