Design Lounge
Install

Nº 299 of 520

Live terminal log card

A streaming service log in a card: coloured levels, multi-select level filters, search highlight, hover-to-hold autoscroll, and copy any line.

Preview

Open full size
1280 × 800 · 20.7 KB · 376 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Live terminal log card" (piece card-terminal-log) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

259 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The log tail of one worker inside an infrastructure console, set as a single card on a warm graphite page. New lines stream in every 0.4–1.3s, each with a timestamp, a coloured level, a source, and a message. Four level chips filter the stream, a search box filters and highlights, and the moment the pointer rests over the log the autoscroll holds so you can read. A pill counts what arrived while you were reading and jumps you back. Any line can be copied with its timestamp and level. The detail worth copying is the hold: autoscroll stops on hover, focus, or scroll-up, never on a timer, and the header badge says which state you are in (LIVE, HOLDING, PAUSED).

This is not terminal-ui-style (a whole visual language) and not audit-activity-log (a human audit trail). It is one data card for a machine stream.

Reference behaviour

  1. First frame: 46 lines already in the log, scrolled to the bottom. Header reads ninefold / eu-west-2 / deploy 4812 above ingest-worker-3 and a cyan LIVE badge with a pulsing 6px dot. Right side shows the line rate (1.0/s, lines in the last 10s) and a Pause stream button. All four level chips are on, each with its count.
  2. Streaming: a new line is appended every 380–1300ms (random). Level mix: 28% debug, 52% info, 14% warn, 6% error. Each new line fades and rises 6px over 380ms. The log keeps the last 240 lines; older lines are removed from the top.
  3. Following: while the log is following, every new line scrolls the log to the bottom instantly.
  4. Hold: pointer enters the log body (mouse only), or focus enters the log, or the user scrolls more than 24px up from the bottom. Autoscroll stops. The badge turns amber and reads HOLDING. Lines still arrive. Each new visible line increments an amber pill at the bottom centre: 3 new lines.
  5. Release: pointer leaves and focus is outside and the log is at the bottom → badge back to LIVE, unseen count resets, log jumps to the bottom. Clicking the pill scrolls to the bottom (smooth, or instant with reduced motion) and clears the count.
  6. Pause stream: the header button toggles generation. Pressed state is a filled amber button labelled Resume stream with a play icon. Badge reads PAUSED in muted grey with a still dot. Resuming restarts the stream after 300ms.
  7. Level chips: each toggles its level (multi-select). Off chips show a hollow square and muted text. Hidden lines are display: none, counts keep counting.
  8. Search: typing filters to lines whose message contains the query (case-insensitive, 90ms debounce) and wraps every match in an amber <mark>. Escape clears. / anywhere focuses the search.
  9. Copy: hovering a line reveals a copy icon at the right edge. Clicking copies HH:MM:SS.mmm LEVEL [source] message. The icon becomes a cyan check for 1.6s, and the footer status reads Copied line 00:14:45.934 for the same 1.6s. If the clipboard is blocked, the status says so instead of pretending.
  10. Keyboard in the log: Tab to the log, then Up/Down move a current-line marker (amber left border, tinted row), Home/End jump, C or Enter copies the current line, Escape clears the marker and leaves.
  11. Footer status always describes the state: 52 lines · 14 matches for "partition" · autoscroll held.
  12. Empty filter result: a centred line reads No lines match "x" at the selected levels. or All levels are hidden. Turn one back on above.

Structure

1280 × 800, page #161513 with 24px dot grid at 5%; card centred, 40px page padding
┌──────────────────────────── card 960 × 640, r 10px ─────────────────────────────┐
│ ninefold / eu-west-2 / deploy 4812                       1.0/s   [ ‖ Pause stream ]│ head 18/20px pad
│ ingest-worker-3  (● LIVE)                       lines, last 10s                 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ [■ DEBUG 14] [■ INFO 27] [■ WARN 3] [■ ERROR 4]              [⌕ Search messages /] │ tools 10/20px
├─────────────────────────────────────────────────────────────────────────────────┤
│ 00:14:03.877  DEBUG  lease        lease renewed partition=11 owner=worker-3    ⧉ │
│ 00:14:04.451  INFO   checkpoint   rebalanced: now owning 6 of 32 partitions      │ log well #191816
│ 00:14:04.944  WARN   upstream     slow flush 3485ms (threshold 1000ms) …         │ 104 / 54 / 124 / 1fr / 36
│ 00:14:09.806  ERR    consumer     write failed: connection reset by peer …       │
│                         ( ↓ 3 new lines )                                        │ pill bottom 14px
├─────────────────────────────────────────────────────────────────────────────────┤
│ 48 lines · 48 shown · following                 [/] search  [↑][↓] move  [C] copy │ foot 10/20px
└─────────────────────────────────────────────────────────────────────────────────┘
  • section.card labelled by the h1. Grid rows auto auto minmax(0,1fr) auto, overflow: hidden.
  • Head: crumb p, h1 containing the worker name and the status badge span. Rate block is aria-hidden decoration. Pause is a button[aria-pressed].
  • Tools: div[role=group][aria-label="Show levels"] of four button[aria-pressed] chips, then a label wrapping a visually hidden name, a search icon, input[type=search], and a kbd hint that hides when the input has text.
  • Body: ul.log[role=log][aria-live=off][tabindex=0] with aria-activedescendant pointing to the current line. Each line is li.ln with five grid cells: .ts, .lv, .src, .msg, and button.cp[tabindex=-1]. The jump pill is a button absolutely positioned over the body.
  • Footer: span[role=status] and an aria-hidden keys legend.

Message copy is generated from templates per level. Use these so the stream reads true:

LevelSourcesTemplates
debugconsumer, cache, runtime, leasepoll queue=events.raw batch=N lag=Nms · cache hit key=tenant:N:schema ttl=Ns · gc pause N.Nms heap=NMB · lease renewed partition=N owner=worker-3
infowriter, api, checkpoint, schema, coordinatorflushed N rows to warehouse.events_2026_10 in Nms · POST /v2/ingest 202 Nms req=hex6 · checkpoint committed offset=N partition=N · schema vN accepted for source checkout-web · rebalanced: now owning N of 32 partitions
warnwriter, upstream, consumer, validateslow flush Nms (threshold 1000ms) table=events_2026_10 · retrying upstream fetch attempt=N/5 backoff=Nms · queue depth Nk above soft limit 10k · dropped N rows: missing field "occurred_at"
errorwriter, api, consumerwrite failed: connection reset by peer (pg-replica-2:5432) req=hex6 · payload rejected: 413 body N.NMB > 5MB limit · deadline exceeded after 30000ms on partition=N

Table names and source names inside messages are wrapped in <em> and coloured cyan.

Tokens

:root {
  /* surfaces: warm graphite, never pure black */
  --bg: #161513;        /* page */
  --card: #1e1d1a;      /* card head, tools, foot */
  --well: #191816;      /* log body and search field */
  --line: #2e2c28;      /* hairlines between regions */
  --line-2: #3a3833;    /* control borders */

  /* ink */
  --ink: #ebe6da;       /* messages, title */
  --ink-2: #aaa395;     /* sources, debug messages, footer bold */
  --ink-3: #8e877a;     /* timestamps, crumb, placeholders */

  /* levels */
  --debug: #948e82;
  --info: #7fbcc8;      /* also <em> highlights and the LIVE badge */
  --warn: #e6b350;
  --error: #f0705a;     /* level label; error messages use #f6c7bc */

  --accent: #e6b350;    /* focus, mark, pill, current line, pressed pause */

  --ui: "Familjen Grotesk", system-ui, sans-serif;
  --mono: "Martian Mono", ui-monospace, monospace;   /* at font-stretch 87.5% */

  --r-card: 10px; --r-ctl: 6px; --r-chip: 5px;
  --pad-x: 20px;
  --row-cols: 104px 54px 124px minmax(0, 1fr) 36px;

  --ease: cubic-bezier(.2, .7, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);
  --t-line: 380ms;  --t-pill: 240ms;  --t-micro: 150ms;
  --shadow-card: 0 40px 80px -40px rgba(0, 0, 0, .7);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
CrumbMartian Mono 87.5%11px5001.40.02emlower
Worker name (h1)Familjen Grotesk22px7001.15-0.01emas written
BadgeMartian Mono 87.5%11px50010.04emUPPER
Rate numberMartian Mono 87.5%15px5001.30—
ButtonFamiljen Grotesk13px60010Sentence
ChipMartian Mono 87.5%11px500 (count 400)10.06emUPPER
SearchMartian Mono 87.5%12px40010—
Log lineMartian Mono 87.5%12.5px400 (level 500)1.6level 0.04emlevel UPPER
FooterMartian Mono 87.5%11px400 (bold 500)1.40sentence
PillFamiljen Grotesk12px60010sentence

Load Martian Mono with the width axis: family=Martian+Mono:wdth,wght@87.5,400;87.5,500. At 100% width the columns get too wide for 960px.

Motion

ElementTriggerPropertyFrom → ToDurationEasingReduced motion
New lineappendedopacity, translateY0, 6px → 1, 0380ms--exponone, appears
LIVE dotalways while streamingopacity1 → .25 → 11.6s loop--easestatic
Jump pillunseen > 0 and holdingopacity, translateY0, 12px → 1, 0240ms--expoinstant
Chips, buttonshover / pressbackground, border—150ms--easeinstant
Copy iconline hover / currentopacity0 → 1120ms--easeinstant
Jump to bottompill clickscrollTopcurrent → bottombrowser smooth—behavior: auto
Autoscrollnew line, followingscrollTopinstant0—same

No other motion. Autoscroll is never animated: smooth-scrolling every 600ms makes the log swim.

States

  • Following (LIVE): cyan badge, pulsing dot, log pinned to the bottom.
  • Holding (HOLDING): amber badge, dot still pulses, new lines append below the fold, pill shows the count.
  • Paused (PAUSED): grey badge, dot still, no new lines, pause button filled amber and reads Resume stream.
  • Chip on: filled square in the level colour, text --ink, border 45% level colour, background 9% level colour. Off: hollow square, text --ink-3.
  • Line hover: row background rgba(235,230,218,.035), copy icon visible.
  • Current line: 2px amber left border, row background rgba(230,179,80,.07), copy icon visible.
  • Copied: icon becomes a cyan check for 1.6s; footer status says Copied line <ts>.
  • Copy blocked: icon unchanged, footer says Copy blocked by this frame. Select the line text instead.
  • Search active: kbd hint hidden, matches marked amber on #1a1813 text.
  • Empty: centred mono line in --ink-3, 48px vertical padding.
  • Focus-visible: 2px amber outline, 2px offset; the log gets outline-offset: -2px so it is not clipped by the card.

Accessibility

  • The log is role="log" with aria-live="off": announcing a line every second is noise. The footer role="status" carries state changes (filters, copy, hold).
  • The log is a single tab stop. Up/Down/Home/End move aria-activedescendant; C or Enter copies; Escape leaves. Copy buttons are tabindex="-1" so a 240-line log is not 240 tab stops.
  • aria-label on the log names the keys: “Log lines. Arrow keys move, C copies the line.”
  • Chips are toggle buttons with aria-pressed, inside a labelled group.
  • The search input has a visually hidden label “Search log”. / focuses it from anywhere except when already typing.
  • Hover hold is mouse-only (pointerType === 'mouse'). Keyboard users get the same hold through focus.
  • Level is never colour-only: the word DEBUG/INFO/WARN/ERR is always printed.
  • Contrast on #191816: --ink 14.2:1, --ink-2 7.1:1, --ink-3 5.0:1 (4.7:1 on --card), info 8.4:1, warn 9.2:1, error 6.1:1. Do not darken --ink-3 below #8e877a; #847d70 falls to 4.35:1.

Responsive rules

  • ≥ 1280: card 960 × 640, five columns as above.
  • 1024: card fills width minus 64px; columns unchanged; search shrinks first (flex: 0 1 280px).
  • 768: same as 1024; chips may wrap to a second row before the search.
  • < 760: page padding 16/12px, card height 100vh − 32px. Log font 11.5px. Columns become 58px 38px minmax(0,1fr) 28px: the source column and the milliseconds are hidden. Search takes its own full row. Rate block and keys legend hide. Pause button becomes a 40px icon button with its text visually hidden. h1 drops to 18px and the badge may wrap under it.
  • Messages wrap (white-space: pre-wrap; overflow-wrap: anywhere). Nothing scrolls sideways.

Acceptance checklist

Always

  • Lines stream on a randomised timer (not a fixed interval) and the buffer is capped.
  • Autoscroll holds on mouse hover, on focus inside the log, and on scroll-up of more than 24px; it resumes only when all three are clear.
  • A badge shows the state in words: LIVE, HOLDING, PAUSED.
  • While holding, a pill counts unseen visible lines and jumps to the bottom on click.
  • Level chips are multi-select toggles with aria-pressed and live counts.
  • Search filters and wraps matches in <mark>; the query is regex-escaped.
  • Copy writes timestamp, level, source and message; the success or failure is shown, not assumed.
  • The log is one tab stop with arrow-key line navigation.
  • Hidden lines are really hidden ([hidden]{display:none} beats display:grid).
  • Reduced motion: no line fade, no dot pulse, instant jump.
  • No horizontal scroll at 375px.

This demo

  • Worker ingest-worker-3, crumb ninefold / eu-west-2 / deploy 4812.
  • Page #161513, card #1e1d1a, log well #191816.
  • Levels: debug #948e82, info #7fbcc8, warn #e6b350, error #f0705a.
  • Martian Mono at 87.5% width for all log text; Familjen Grotesk for the title, buttons, pill.
  • 46 seed lines, 240-line cap, 380–1300ms between lines.

Implementation notes

The hold is three booleans, not one. Hover, focus and scroll-up each hold independently; following is “none of them”:

let hovering = false, focused = false, scrolledUp = false, unseen = 0;
const following = () => !(hovering || focused || scrolledUp);

body.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') hovering = true; sync(); });
body.addEventListener('pointerleave', () => { hovering = false; resume(); });
log.addEventListener('focusin', () => { focused = true; sync(); });
log.addEventListener('focusout', e => { if (!log.contains(e.relatedTarget)) { focused = false; resume(); } });
log.addEventListener('scroll', () => {
  scrolledUp = log.scrollHeight - log.scrollTop - log.clientHeight > 24;
  sync();
}, { passive: true });
function resume() { if (following()) { unseen = 0; log.scrollTop = log.scrollHeight; } sync(); }
// on each new line:
if (following()) log.scrollTop = log.scrollHeight; else if (!row.hidden) unseen++;

Highlight from plain text, never from the HTML. Keep the message as plain text and as rich HTML. With no query, render the rich version. With a query, escape the plain text and wrap matches:

const esc = s => s.replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
function msgHTML(row) {
  if (!query) return row.html;
  const re = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
  return esc(row.text).replace(re, '<mark>$1</mark>');
}

Copy with an honest fallback. Sandboxed iframes often block navigator.clipboard:

async function copy(text) {
  try { await navigator.clipboard.writeText(text); return true; } catch {}
  const ta = Object.assign(document.createElement('textarea'), { value: text });
  ta.style.cssText = 'position:fixed;opacity:0';
  document.body.appendChild(ta); ta.select();
  let ok = false; try { ok = document.execCommand('copy'); } catch {}
  ta.remove(); return ok;
}

Common mistakes:

  • display: grid on the row overriding the hidden attribute. Add .ln[hidden]{display:none}.
  • Smooth-scrolling on every line. The log drifts and never settles. Jump instantly; smooth only for the pill.
  • role="log" with the default polite live region. Screen readers read every line forever.
  • Building the regex from raw input. A user typing ( throws.
  • Pausing autoscroll on a timeout after hover. Hold until the pointer leaves.
  • One copy button per line in the tab order.
  • A fake macOS window bar with three dots. This is a card in a console, not a window.

Rebuild order:

  1. Card grid and the four regions with tokens.
  2. Line template, seed 46 lines, scroll to bottom.
  3. Randomised stream with the 240-line cap.
  4. Hold logic, badge, pill.
  5. Chips and search with highlight.
  6. Copy, keyboard current line, status messages.
  7. The < 760 layout and reduced motion.

Details

Palette
Type
Martian Mono · Familjen Grotesk
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

The F-List

Make the reveal the message. Bars lift off the text, and badges explain the list.

All sources