feat(ui): add security banner markup + styles (approved variant A)

HTML + CSS for the canary leak / ML block banner. Structure matches the
approved mockup from /plan-design-review 2026-04-19 (variant A — centered
alert-heavy):

  * Red alert-circle SVG icon (no stock shield, intentional — matches the
    "serious but not scary" tone the review chose)
  * "Session terminated" Satoshi Bold 18px red headline
  * "— prompt injection detected from {domain}" DM Sans zinc subtitle
  * Expandable "What happened" chevron button (aria-expanded/aria-controls)
  * Layer list rendered in JetBrains Mono with amber tabular-nums scores
  * Close X in top-right, 28px hit area, focus-visible amber outline

Enter animation: slide-down 8px + fade, 250ms, cubic-bezier(0.16,1,0.3,1) —
matches DESIGN.md motion spec. Respects `role="alert"` + `aria-live="assertive"`
so screen readers announce on appearance. Escape-to-dismiss hook is in the
JS follow-up commit.

Design tokens all via CSS variables (--error, --amber-400, --amber-500,
--zinc-*, --font-display, --font-mono, --radius-*) — already established in
the stylesheet. No new color constants introduced.

JS wiring lands in the next commit so this diff stays focused on
presentation layer only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-19 19:18:57 +08:00
parent f68fa4a9ee
commit a9f702a715
2 changed files with 158 additions and 0 deletions
+132
View File
@@ -87,6 +87,138 @@
flex: 1; flex: 1;
} }
/* ─── Security Banner ─────────────────────────────────────────────
Variant A approved in /plan-design-review 2026-04-19. Centered
alert-heavy. Fires on security_event — canary leaks + ML BLOCK
verdicts. Trust UX: layer names + confidence scores in mono so
the user can see exactly WHY the session was terminated.
*/
.security-banner {
position: relative;
padding: 20px 16px;
text-align: center;
background: rgba(20, 20, 20, 0.98);
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
animation: securityBannerEnter 250ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes securityBannerEnter {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.security-banner-close {
position: absolute;
top: 6px;
right: 6px;
width: 28px;
height: 28px;
background: transparent;
border: none;
color: var(--zinc-500, #71717A);
font-size: 20px;
line-height: 1;
cursor: pointer;
border-radius: var(--radius-md, 8px);
padding: 0;
}
.security-banner-close:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--zinc-300, #D4D4D8);
}
.security-banner-close:focus-visible {
outline: 2px solid var(--amber-500);
outline-offset: 2px;
}
.security-banner-icon {
color: var(--error);
display: flex;
justify-content: center;
margin-bottom: 8px;
}
.security-banner-title {
font-family: var(--font-display, 'Satoshi', sans-serif);
font-weight: 700;
font-size: 18px;
color: var(--error);
margin-bottom: 2px;
}
.security-banner-subtitle {
font-family: var(--font-body, 'DM Sans', sans-serif);
font-size: 13px;
color: var(--zinc-400, #A1A1AA);
margin-bottom: 12px;
}
.security-banner-expand {
display: inline-flex;
align-items: center;
gap: 6px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-md, 8px);
padding: 6px 12px;
color: var(--zinc-300, #D4D4D8);
font-family: var(--font-body, 'DM Sans', sans-serif);
font-size: 12px;
cursor: pointer;
}
.security-banner-expand:hover {
background: rgba(255, 255, 255, 0.04);
}
.security-banner-expand:focus-visible {
outline: 2px solid var(--amber-500);
outline-offset: 2px;
}
.security-banner-chevron {
transition: transform 200ms ease-out;
}
.security-banner-details {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
text-align: left;
}
.security-banner-section-label {
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--zinc-500, #71717A);
margin-bottom: 6px;
}
.security-banner-layers {
display: flex;
flex-direction: column;
gap: 4px;
}
.security-banner-layer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.02);
border-radius: var(--radius-sm, 4px);
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-size: 12px;
}
.security-banner-layer-name {
color: var(--zinc-300, #D4D4D8);
}
.security-banner-layer-score {
color: var(--amber-400);
font-variant-numeric: tabular-nums;
}
.conn-btn { .conn-btn {
font-size: 9px; font-size: 9px;
font-family: var(--font-mono); font-family: var(--font-mono);
+26
View File
@@ -14,6 +14,32 @@
</div> </div>
</div> </div>
<!-- Security event banner — fires on prompt injection detection.
Variant A from /plan-design-review 2026-04-19: centered alert-heavy,
big red error icon, mono layer scores in expandable details. -->
<div class="security-banner" id="security-banner" role="alert" aria-live="assertive" style="display:none">
<button class="security-banner-close" id="security-banner-close" aria-label="Dismiss">&times;</button>
<div class="security-banner-icon" aria-hidden="true">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<div class="security-banner-title" id="security-banner-title">Session terminated</div>
<div class="security-banner-subtitle" id="security-banner-subtitle">prompt injection detected</div>
<button class="security-banner-expand" id="security-banner-expand" aria-expanded="false" aria-controls="security-banner-details">
<span>What happened</span>
<svg class="security-banner-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="security-banner-details" id="security-banner-details" hidden>
<div class="security-banner-section-label">SECURITY LAYERS</div>
<div class="security-banner-layers" id="security-banner-layers"></div>
</div>
</div>
<!-- Browser tab bar --> <!-- Browser tab bar -->
<div class="browser-tabs" id="browser-tabs" style="display:none"></div> <div class="browser-tabs" id="browser-tabs" style="display:none"></div>