📐 Sibyl Dashboard — Canonical Component Styleguide
The single source of truth for the dashboard's reusable component vocabulary. This catalog is Sibyl-owned and hand-maintained — it is no longer a Prometheus port (the sync script is retired). When applying the styleguide to the app or building a new component, this page is authoritative: the app should change to match this, not vice-versa.
Action toolbar · Avatar / user chip · Batch action bar · Breadcrumb page header · Buttons · Cards · Charts (Chart.js) · Code & markdown surfaces · Collapsible content sections · Color tokens · Confirmation gate · Expandable table rows · Forms · Inline divider · Layout grids · Loading & empty states · Modal · Mode switch · Nav count badge · Numeric formatting · Pill-filter row · Pills & status badges · Progress bars · Search + dropdown filter bar · Segmented stat cards · Side panel / drawer · Sidebar navigation — 4-level expandable header system · Sparklines · Stat grid · Status dot · Sticky apply bar · Tables · Tabs · Toast · Typography
Action toolbar — CANONICAL page-level action bar
The horizontal action bar at the top of a page — Back on the left, a vertical divider, then per-record actions grouped by kind, with an optional utility affordance on the right.
- What
- A single
.cardrow of outline buttons with a flat single-color Lucide SVG icon on each. Layout: Back (blue outline, ←) → a.button-toolbar-divider(1px vertical line) → action buttons grouped by kind in fixed order (lifecycle → note → investigation → read) → optional right-side affordance (Generate summary, Export, etc.). The Back divider is the only inter-button separator; kind groups inside the action span carry no visible divider. - When
- Top of a page that operates on a single record (item detail, schedule detail, dig detail).
- Placement
- Default: directly below the page title / breadcrumb header. If the page has tabs, the toolbar lives under the tab row instead — it becomes the first thing the operator sees after switching tabs. That way, each tab can swap in its own action set (a Notes tab surfaces note actions, a Digs tab surfaces investigation actions, etc.) without making the toolbar above the tabs choose a one-size-fits-all action list.
- When not
- List/index pages → no toolbar (those use header-row buttons via the breadcrumb header's right-side action slot). Modals → use a
.button-row.spreadfooter. Per-row table actions → see Tables → Row actions. - Sizing
- card padding 10px 14px · margin-bottom 12px · flex, align-items center, flex-wrap wrap, gap 6px · each button padding 6px 14px · radius 4px · 12px font · transparent background, 1px colored border, color matches border (outline-only). Icons use
.icon(14px, flat single-color Lucide SVG,stroke="currentColor"so they inherit the button's kind color). The Back divider is.button-toolbar-divider(1px × 22px,var(--border)). - Variants
- Pick whatever color reads as appropriate for the action — both the role-based palette (
.primaryblue,.dangerred,.ghostmuted) and raw color tokens (var(--blue)/--green/--yellow/--red/--muted/--text) are fair game. One useful default scheme — used in the example below and initem-detail.js— keys color by action kind:lifecycle→var(--blue)— Back, Triage, Start, Block, Unblock, Defer, Closenote→var(--text)— Add note, Mark notes seen, Mark notes processedinvestigation→var(--green)— Spawn dig, Synthesize, Re-triage, Replay proberead→var(--muted)— Copy link, Open in CLI, Dependent items, Probe target
disabledwith cursor:not-allowed and a "doing X…" label. - Do
- ✓ Always lead with Back followed by a
.button-toolbar-divider· use a flat Lucide SVG on every button · use outline style (transparent bg + colored border + colored text) for every button in the bar - Don't
- ✗ Use emoji icons (⛔ ✅ 📝 🔬 🔗 etc.) instead of flat SVG icons
sibyl-reviews/src/public/pages/item-detail.js:_idRenderActionBarCard (the Back / Block / Defer / Close / Add note / … / Generate summary bar at the top of every item-detail page).<div class="card" style="padding:10px 14px;margin-bottom:12px;display:flex;gap:6px;align-items:center;flex-wrap:wrap"> <button style="…outline blue…"><svg class="icon" …>…arrow-left…</svg> Back</button> <span class="button-toolbar-divider"></span> <!-- lifecycle group (blue) --> <button style="…outline blue…"><svg class="icon" …>…ban…</svg> Block</button> <button style="…outline blue…"><svg class="icon" …>…pause…</svg> Defer</button> <button style="…outline blue…"><svg class="icon" …>…check…</svg> Close</button> <!-- note group (text color) --> <button style="…outline text…"><svg class="icon" …>…file-plus…</svg> Add note</button> <!-- investigation group (green) --> <button style="…outline green…"><svg class="icon" …>…flask…</svg> Spawn dig</button> <!-- read group (muted) --> <button style="…outline muted…"><svg class="icon" …>…link…</svg> Copy link</button> <!-- right-side affordance --> <button style="…outline muted…">Generate summary</button> </div>
Back sits alone on the left, separated by a 1px .button-toolbar-divider. The right-side Generate summary affordance uses the muted-border treatment until a summary exists.
Avatar / user chip
28px circular identity chip — photo if avatar_path is set, otherwise an initial on var(--blue).
- What
- 28px
border-radius:50%chip. Photo variant:<img>withobject-fit:cover. Initial fallback: uppercase first letter on a solidvar(--blue)background, white text, centered. - Seen at
- sidebar user block
index.html:212; Users adminusers.js:121–124,users.js:214.
Batch action bar
Header strip above a table that appears only when one or more checkbox-selected rows exist — shows a contextual action button.
- What
- Flex row, initially
display:none; flips todisplay:flexwhen selection count > 0. Carries action button(s) labeled with the count ("Investigate Selected (3)"). - Seen at
whale-watcher.js:599,whale-watcher.js:766,whale-watcher.js:1469.- Distinct from
- The action toolbar (page-level, always present); this bar is table-local and selection-driven.
Cards
General-purpose container for grouped content.
- What
.card— surface panel with border + radius + padding. Optional.card-header/.card-footerslots.- When
- Group related content where a full-width table is overkill; KPI tiles via the
.label/.value/.subchildren. - When not
- Full-width tabular data → use
.table-wrap. A grid of metrics → use the.stats-grid. - Sizing
- padding 16px · radius 8px · border 1px
var(--border)· bgvar(--surface). In a grid useminmax(240px,1fr)columns, 12px gap. - Variants
.card-header(flex title/action row) ·.card-footer(top-bordered action row) ·.label/.value/.subKPI children.- Do
- ✓ Use
.cardfor any bordered surface panel - Don't
- ✗ Hand-roll
style="background:var(--surface);border:1px solid var(--border);border-radius:8px"
whale-watcher.js:891, users.js:687, platform-config.js:218, backtest.js:614).<div class="card"> <div class="card-header"><h3>Title</h3><span class="pill yes">active</span></div> <div class="label">Net P&L</div><div class="value">$12,480</div> </div>
Bare card — surface, border, padding.
With header
activeWith footer
Body content.
Charts (Chart.js)
Chart.js conventions: dark theme, disabled legend by default, custom tooltips, fixed-height container. Heavy usage across 20+ pages but no canonical spec yet.
- Types in use
- line, bar (stacked + grouped), scatter / bubble, doughnut.
- Conventions
- Dark canvas; grid lines =
var(--border); ticks =var(--muted); legend usually disabled; tooltipcallbacks.labeloverridden for unit-aware formatting; canvas wrapped in a fixed-heightposition:relativediv. - Seen at
- line
advanced-analytics.js:415; baradvanced-analytics.js:446; doughnutadvanced-analytics.js:543; bubbleadvanced-analytics.js:795,signal-quality.js:271.
Live preview omitted; the styleguide page doesn't bundle Chart.js. Add a working preview once we settle on the canonical chart-options blob.
Code & markdown surfaces
Inline <code>, monospace <pre> blocks, and the .tc-md wrapper used to render LLM markdown.
- Inline code
<code>for config keys, parameter names, short literal values. Seen atstrategy.js:49,backtest.js:899.- Pre block
- Multi-line output (raw JSON, smoke-test logs) — dark bordered surface, Courier New monospace, sometimes with a copy-to-clipboard affordance. Seen at
smoke-test.js:309,strategy-analyst.js:1161. - Markdown wrapper
.tc-md=marked.parse()output with scoped overrides forblockquote,code,pre,details/summary, and heading sizes. Used for LLM-produced content. Seen attriage-console.js:244–277,item-detail.js:3236,claude-sessions.js:68.
Inline: tune HOUR_WEIGHT_PROFILE via the COE.
{
"change_kind": "synthesis_decision",
"confidence_band": "high",
"scope": "SHARED"
}
Collapsible content sections
Native <details>/<summary> blocks used outside the sidebar — for collapsible logs, hidden detail panels, expert-mode toggles.
- What
- Native HTML disclosure block. Sometimes styled with a chevron and inner CSS scope; sometimes bare. The sidebar nav already uses this internally, but standalone content usage is a separate pattern.
- Seen at
crypto-probe.js:1443,tuning-center.js:419,whale-watcher.js:975,triage-console.js:1757.
Color tokens
The full palette. Always use the CSS variable, never a raw hex.
- What
- The dashboard's design tokens, defined on
:root. Values here are aligned 1:1 with the live app shell. - When
- Every color reference in CSS or inline styles.
- When not
- Never hardcode hex (e.g.
#3fb950) — it breaks theming and diverges over time. - Variants
--surface/--blue/--green/--yellow/--redare the canonical names;--panel/--accent/--ok/--warn/--errare legacy aliases (same values) kept only for ported CSS.- Do
- ✓
color: var(--green) - Don't
- ✗
color: #3fb950
whale-watcher.js (#3fb950/#f85149/#d29922/#f0883e) and scattered inline colors.Confirmation gate
Native confirm('…') guard used uniformly before destructive operations — delete, promote, rollback, mass-approve.
- What
- One-line guard:
if (!confirm('Delete user X? This cannot be undone.')) return;. Message phrasing pattern: action verb + object + consequence note. - Seen at
platform-config.js:390,whale-watcher.js:1889,users.js:604,regime-tuner.js:431(15+ call sites total).- Distinct from
- The custom modal (used for multi-step interactions); native confirm is the canonical lower-fi guard for "are you sure?" before destructive verbs.
Example call sites (not rendered — confirm() is a browser dialog):
if (!confirm('Delete user ' + name + '? This cannot be undone.')) return;
if (!confirm('Promote whale candidate ' + id + ' to live tracking?')) return;
if (!confirm('Roll back to revision ' + rev + '? Newer changes will be lost.')) return;
Expandable table rows
Click a row to insert a full-width colspan detail row below it; click again to collapse. Toggle state tracked in a JS Set.
- What
- Row gets a chevron/caret cell; click toggles insertion of an immediately-following
<tr>with one<td colspan=N>holding the detail panel. - Seen at
advanced-analytics.js:1215,overview.js:196,optimization-lab.js:1034,calibration.js:519.- Distinct from
- The drilldown-into-detail-page navigation (which jumps to a new route); expandable rows keep the operator on the same page.
| Market | Edge | Status | |
|---|---|---|---|
| ▾ | BTC > $80k EOD | 4.1% | open |
Detail panel — last 5 fills, slippage stats, source attribution. Closes when the parent row's caret is clicked again. | |||
| ▸ | ETH > $4k EOD | 2.3% | SIM |
| ▸ | Fed cut March | -1.0% | skipped |
Forms
One canonical input/select treatment — surface bg, border, 4px radius. Gallery below shows every common control so we can iterate on what stays / changes.
- What
- Text-class inputs (text, number, search, email, date…),
<textarea>,<select>, plus the native widgets (checkbox, radio, range, file, color). Text/select fields share one treatment:var(--surface)background,var(--border)1px border, 4px radius, 12px text. Native widgets keep their browser-default chrome (checkbox/radio dots, range track, file button) and inherit the page font. - When
- All form controls. One token only for field backgrounds —
var(--surface). - When not
- Don't mix
--bg/--bg2backgrounds (today three different tokens are used interchangeably). - Sizing
- text/select padding 4px 8px · radius 4px · 12px text · label 11px muted uppercase above the field with 4px gap · helper/error text 11px below the field with 4px gap. Textareas: same field treatment +
min-height: 80pxand resize:vertical only. - States
:disableddrops opacity to ~.55 and switches cursor tonot-allowed; readonly keeps full opacity but removes the focus ring.aria-invalid="true"swaps the border tovar(--red). Required fields lead the label with an asterisk styledcolor:var(--red).- Do
- ✓
background:var(--surface);border:1px solid var(--border)for every text and select field · keep labels on their own line above the field (not inline) · use the helper-text slot below the field for unit hints / format examples - Don't
- ✗
--bg/--bg2/--fgfield tokens · floating labels · inline placeholder-as-label (placeholders are examples, not labels)
--surface vs --bg vs --bg2); the backtest-scoped .bt-form-input leaked into users.js.type="search" · browser clear-X · debounce in the handler.
Gallery is a sandbox — every variant is a candidate, not a commitment. Call out which look right, which to change, and which to drop.
Inline divider
1px vertical rule for separating groups of inline controls — used in filter bars, toolbars, header strips.
- What
<span style="width:1px;height:16px;background:var(--border);margin:0 8px">— or the named.button-toolbar-divideralready instyle.cssfor the action toolbar.- Seen at
whale-watcher.js:581,whale-watcher.js:588,whale-watcher.js:595;.button-toolbar-dividerusage in the action toolbar.- Open question
- Two parallel implementations (inline span vs
.button-toolbar-dividerclass) — pick one canonical form during review.
Layout grids
.two-col / .three-col / .cards-N — the responsive grid wrappers that size cards across a page.
- What
.two-colis a 1fr 1fr grid (gap 12px) that collapses to 1 column on narrow viewports;.three-colis 1fr 1fr 1fr;.cards-2through.cards-6are column-count modifiers on the.cardsgrid.- Seen at
strategy.js:151,backtest.js:603,kpi.js:211,whale-watcher.js:2697(two-col);advanced-analytics.js:971(three-col).
Col 1
Col 2
Col 3
Loading & empty states
One loading pattern, one empty pattern — no more 3-way split.
- What
- Loading:
.loading-state+.spinner. Empty:.empty. - When
- Loading while a page/section fetches; empty when a fetch returns no rows.
- When not
- Never use
.emptyas a loading state; never raw inline padding loaders. - Sizing
- spinner 22px · loading-state column, 14px gap, 80px/40px padding, muted 13px · empty 24px padding centered muted.
- Do
- ✓
.loading-state+.spinnerfor every loader - Don't
- ✗
class="loading",.empty-state, or inlinepadding:40pxloaders
.loading plain text (6 files), .empty misused as loader (ab-testing.js:53, regime-tuner.js:602), undefined .empty-state (whale-watcher.js:680), inline loaders (settings.js:184).Modal
One overlay pattern — supersedes 5 hand-rolled variants with a z-index collision.
- What
.modal-backdrop(fixed, centered, z-index 1000) wrapping a.modalpanel with a.modal-header.- When
- Focused confirm/edit flows that must block the page. Backdrop click + a ghost Close button both dismiss.
- When not
- Non-blocking feedback → toast. Inline edits → in-place form.
- Sizing
- panel
min(440px,92vw)· max-height 86vh scroll · padding 20px 24px · radius 8px · backdroprgba(0,0,0,.6)· z-index 1000 (always). - Do
- ✓ One z-index (1000) and
inset:0backdrop - Don't
- ✗ Per-page modals with ad-hoc z-index (e.g. 9999)
app.js:887, users.js:683, data-management.js:1907 [z-index 9999], whale-watcher.js:2801).Mode switch
Binary or 2–3-way pill toggle for an inline state choice — distinct from tabs (page navigation) and pill-filter rows (category narrowing).
- What
- Button group inside a bordered rounded container; the active button gets
background:var(--blue)and white text. - Seen at
overview.js:542,kpi.js:151,settlement.js:112,whale-watcher.js:2104,positions.js:33.- Distinct from
- Tabs switch views; mode-switch flips a single parameter (e.g. ROI vs P&L, SIM vs LIVE, 7d vs 30d).
Numeric formatting
Cross-page conventions for displaying numbers, relative time, and signed deltas. Bundles three closely-related patterns the audit flagged.
- Monospace value
font-family:'Courier New',monospaceon headline numbers (KPI tiles, table-cell amounts) so digits align across rows.- Time-relative labels
- Standardized "Xs ago" / "Xm ago" / "Xh ago" / "Xd Xh ago" via global helpers in
app.js:106–131(fmtAge,fmtAgo,fmtDuration). Reviews has a parallel local variant atitem-detail.js:1069. - Signed-delta coloring
- Positive = leading
++var(--green); negative =var(--red); neutral =var(--muted). Applied to ROI / P&L / win-rate deltas. Seen atkpi.js:1264,strategy-lab.js:591–601,backtest.js:648–651.
Pill-filter row
Clickable pill row for one-axis filtering — via renderCategoryBar() in _shared.js.
- What
- A row of
.pill-filtertoggles;.activemarks the current selection. - When
- Category/segment filtering above a table or chart. Prefer the shared
renderCategoryBar()helper for category fields. - When not
- Non-interactive status →
.pill. Many options / free text → a<select>. - Sizing
- padding 3px 10px · radius 12px · 11px · 6px gap between pills, 12px margin-bottom on the row.
- Do
- ✓ Reuse
renderCategoryBar()output shape - Don't
- ✗ Re-implement a pill-filter row inline per page
calibration.js:400, stock-probe.js:182, profile-attribution.js:108).Pills & status badges
Compact inline status chips with semantic modifier classes.
- What
.pill+ a semantic modifier (.yes/.no/.sim/.live/.offand the.bt-*bet-type set).- When
- Inline state/flags in tables, headers, list rows.
- When not
- Clickable filters →
.pill-filter(next section), not.pill. - Sizing
- padding 1px 7px · radius 10px · 11px/600. Tinted bg at ~15% alpha, solid token text color.
- Variants
.yesgreen ·.nored ·.simyellow ·.livered ·.offmuted ·.bt-*bet types.- Do
- ✓ Pick the modifier by meaning, not color
- Don't
- ✗ Inline
border-radiuschip spans or a local_catPillcopy
whale-watcher.js:115, users.js:139), the duplicated _catPill in kpi.js:116.Progress bars
Two related patterns: a plain linear fill bar, and a composite label-+-bar-+-value score row.
- Linear bar
- 6–8px tall,
var(--border)background, 3–4px radius, colored fillwidth:%. Used for score bars, coverage, AB-test progress, readiness scores. Seen atstrategy.js:140,whale-watcher.js:207,advanced-analytics.js:1418,backtest.js:274. - Score-bar composite
- Three-part flex row: fixed-width muted label → flex fill track + colored fill → right-aligned monospace numeric value. Seen at
strategy.js:138–144,whale-watcher.js:207–225,advanced-analytics.js:1000. - Distinct from
- The segmented bar inside segmented stat cards is a different pattern (multi-segment proportional).
Search + dropdown filter bar — CANONICAL multi-axis table filter
Horizontal toolbar combining free-text search with one or more single-select dropdowns — multi-axis filtering without consuming vertical real estate.
- What
- A
.cardrow with: atype="search"input on the left (debounced, drives aq=query param), then one or more single-select<select>dropdowns whose default option is an icon-prefixed "All …" sentinel ("🏷️ All kinds", "🎯 All priorities"). - When
- Filtering a table where the operator wants to combine quick-typed text refinement (title / id / description) with structured dropdown narrowing (kind / priority / component / system). Each filter contributes one query parameter to the server-paged table below.
- When not
- Few mutually exclusive categories that should be one click → pill-filter row. No text dimension and only one or two booleans → an inline mode-switch.
- Sizing
- row card padding 12px · min-height 56px · 8px gap · flex-wrap · align-items center · search input 240px wide, 6px 10px padding · selects 6px padding · 12px font on every control · 4px radius · 1px border
var(--border)· bgvar(--bg)(NOT--surface) so controls read as inputs against the card. - Variants
- Default sentinel options always lead with a category icon ("🏷️ All kinds", "🎯 All priorities", "📦 All components", "📋 All systems"). Per-option labels carry their own icon ("🔴 HIGH", "🐛 finding"). A boolean refinement always becomes its own two-option dropdown ("All / Only X") — never a trailing checkbox + label, which eats horizontal real estate for one on/off bit.
- Do
- ✓ Use
type="search"(gets the browser clear-X for free) · debounce the input handler · icon-prefix every dropdown sentinel · keep all controls on one wrapping row - Don't
- ✗ Stack the search above the dropdowns · drop a "Filter" submit button (filtering should be live) · use surface-colored controls (they blend into the card) · add a trailing checkbox + caption for a boolean filter (promote it to a 2-option dropdown instead)
sibyl-reviews/src/public/pages/triage-console.js (decisions / items / revisits sub-tabs all share this exact bar — see _tcRenderInboxDecisionsView).<div class="card" style="padding:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;min-height:56px">
<input type="search" placeholder="Search title, rationale, recommendation…"
style="padding:6px 10px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:240px;font-size:12px">
<select style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-size:12px" title="Item kind">
<option>🏷️ All kinds</option>
<option>🐛 finding</option>
<option>📋 spec</option>
<option>📡 probe</option>
</select>
<!-- additional dropdowns: priority, component, system… -->
</div>Segmented stat cards — CANONICAL drill-through header row
Compact card showing a total, a proportional segmented bar, and a clickable bubble row that deep-links to a filtered table.
- What
- A
.cardwith four stacked rows: title row (label on the left, total right-justified) → one-line muted caption → 8px segmented bar where each segment's width = its share of the total → wrap row of color-coded badge bubbles, one per segment, each clickable to jump to a pre-filtered view of the linked table. - When
- Showing a population (open backlog, 24h activity, errors by class) AND letting the operator drill into any slice in one click. Two to four cards make a "command-center" header above a table.
- When not
- Single headline number → bare
.cardwith.label/.value. 4–8 unrelated KPIs → the stat grid. Time-series → chart. - Sizing
- card flex-basis 200–240px · padding 8px 12px · 6px gap between rows · label 10px uppercase muted with .04em tracking · total 12px/700 right-justified · caption 10px muted with
margin-top:-4px· bar 8px tall, 4px radius, 1px border,var(--bg)fill behind segments · bubbles 2px 8px · 10px/600 · 10px radius · 6px gap. - Variants
- Bar segment + bubble share one canonical palette per dimension (priority, kind, activity bucket). Bubbles carry a count prefix ("12 high") and an
onclickthat filters the linked table; zero-count bubbles stay clickable and jump to an empty filtered view (the affordance is the point). - Do
- ✓ Right-justify the total on the label line · use one shared palette across bar + bubbles · keep the bar at exactly 8px tall
- Don't
- ✗ Pile the total on its own line · use different colors in the bar than in the bubbles · invent a third row of mini-numbers below the bubbles
.stats-grid headers that flatten priority/kind into equal-weight boxes. Live in sibyl-reviews/src/public/pages/triage-console.js:_tcRenderInboxStats (above the Decisions table).Side panel / drawer
Right-edge full-height fixed panel for conversational / contextual content. Distinct from modal (centered overlay).
- What
position:fixed;right:0;top:0;bottom:0panel, 360–500px wide,border-left:1px solid var(--border), drop shadow. Slides in over main content; dismissed by clicking backdrop or a close button.- Seen at
- chat panel at
triage-console.js:1022–1038,triage-console.js:1765. - Distinct from
- The modal (centered, modal-blocking); the drawer is edge-anchored and full-height for ongoing context.
Sparklines
Inline SVG mini-charts (~60×14) for time-series data in compact cards — no Chart.js dependency.
- What
- Tiny normalized
<svg><polyline>rendered by a local helper. Distinct from full Chart.js canvas because they sit inline with text and don't need axes or legends. - Seen at
system-health.js:1518–1529,strategy.js:96,whale-watcher.js:2606.
Stat grid
The canonical "row of headline numbers" — via renderParamsGrid() in _shared.js.
- What
.stats-gridof.stat-boxtiles, each.s-label+.s-value(+ optional.s-sub).- When
- 3–8 related metrics at the top of a page/section. Prefer the shared
renderParamsGrid()helper. - When not
- A single number → a
.cardwith.label/.value. Tabular data → table. - Sizing
- grid
repeat(4,1fr), gap 12px · tile padding 14px · value 18px/700 mono. - Do
- ✓ Route through
renderParamsGrid() - Don't
- ✗ Use the legacy
.label/.value/.sub-in-card pattern for metric rows
.stat-box (alpha-hunter.js:184), .label/.value/.sub tiles in 18 files, inline font-size:32px numbers (system-health.js:170).Status dot
8px solid color-coded health indicator — sits next to a label as a textless cue.
- What
.dotclass with.fresh(green) /.stale(yellow) /.dead(red) modifiers; 8px circle,flex-shrink:0. Also appears with inline backgrounds for per-instance colored dots.- Seen at
- definition at
style.css+index.html:386–389; usage atorchestrator.js:131,platform-config.js:453,instance-selector.js:76. - Distinct from pills
- Pills carry text; dots are pure-color cues that sit before a separate label.
Sticky apply bar
A position:sticky;top:0 strip that appears above content when there are unsaved settings changes — count + Save + Revert.
- What
- Sticky bar with pending-change count, a primary "Save" button, and (on platform-config) a revert affordance. Hidden when zero changes pending.
- Seen at
settings.js:221–246,platform-config.js:210.- Distinct from
- The action toolbar (always present on detail pages); this bar appears/disappears based on dirty state.
Tables
Data grid — always wrapped in .table-wrap and wired via makeTableInteractive().
- What
.table-wrap><table>with<th class="sortable" data-col>headers;makeTableInteractive(id, …)adds sort + pagination + sticky header.- When
- Every tabular dataset — even read-only ones (still wrap + register so scroll/paging is uniform).
- When not
- A handful of metrics →
.stats-grid. Key/value pairs → a.card. - Sizing
- 12px cells · uppercase 11px muted headers · radius 8px on the wrap · horizontal scroll on overflow.
- Variants
- Sort via
createSortState()· server paging via theremoteoption ·noPaginationfor tiny tables. - Row actions
- Per-row buttons (Edit, Delete, Investigate…) live in a single far-right "Actions" column — never scattered inline with data cells. The column is shrunk to fit the buttons via
width:1%on the<th>(table-auto layout treats this as "as narrow as content allows" and gives leftover width to the data columns); cells inherit the table'swhite-space:nowrapso the button row never wraps. With the column shrunk,text-align:centeron the header centers "Actions" directly over the button row. Wrap the buttons in a plaindisplay:inline-flex;flex-wrap:nowrap;gap:6px;justify-content:flex-endcontainer — not.button-row(its defaultflex-wrap:wrapwould stack the buttons two-deep once the column tightens). Row buttons are sized one notch tighter than the canonical button (padding:2px 8px·font-size:11px·gap:4px· 12px icon) so they don't dominate the row visually. Every button carries a flat Lucide SVG icon (.icon,stroke="currentColor") so the glyph inherits the button's role color. Non-sortable header (no.sortable, no.sort-caret). - Do
- ✓ Give every table an
idand pass it through the helper · put row actions in one far-right Actions column with right-justified buttons and a centered header - Don't
- ✗ Anonymous
.table-wrapwith no helper, or custom pager buttons · scatter buttons across multiple data columns or left-align the action cell (drifts away from the table edge as the column widens)
.table-wrap tables bypassing the helper (advanced-analytics.js:210, strategy-lab.js:141); custom pagers (ab-testing.js:843, settings.js:531); per-cell inline action buttons (users.js:412, whale-watcher.js:687) that the eye has to hunt for.| Market ▾ | Edge ▾ | Status ▾ | Actions |
|---|---|---|---|
| BTC > $80k EOD | 4.1% | open | |
| ETH > $4k EOD | 2.3% | SIM | |
| Fed cut March | -1.0% | skipped | |
| NYC rain Friday | 3.7% | open | |
| SOL > $200 | 1.2% | open | |
| NFL Sunday over | 0.8% | SIM | |
| Senate confirm | -0.5% | skipped | |
| CPI > 3.2% | 2.9% | open | |
| SPX close > 5300 | 1.7% | open | |
| BTC > $100k EOY | 5.4% | SIM | |
| AVAX > $40 | 0.3% | skipped | |
| LA wildfire ext | -1.4% | skipped | |
| SCOTUS ruling | 2.1% | open | |
| UK election early | 1.9% | SIM |
14 rows · default 10/page. Switch to 25 or All to land on "Page 1 of 1" with all four nav arrows disabled — the canonical page-bound state. The far-right Actions column shows the canonical row-button placement: centered header, right-justified buttons.
Tabs
In-page section switcher.
- What
.tabscontainer of.tabitems, each with an emoji.tab-icon+ label;.tab.activeis the current one (blue underline). The.tabsrow always lives inside a horizontal-scroll wrapper (overflow-x:auto;white-space:nowrap;flex-wrap:nowrap) so tabs never wrap to a second line on narrow viewports — they scroll horizontally instead.- When
- Switching views within one page (sub-pages of a hub, facets of a dataset). Every tab carries an emoji icon — tabs are wayfinding, so they use the emoji language (like the sidebar/breadcrumb), not button SVGs.
- When not
- Cross-page navigation → the sidebar. Binary mode →
.mode-switch. - Sizing
- tab padding 8px 16px · 13px · 6px icon gap · icon 14px · bottom-border 2px accent when active. Row has its own bottom border + 16px margin-bottom (don't override with
margin-top:0). Scroll wrapper has no extra padding — the tab bar's own padding/border still aligns with the page gutter. - Do
- ✓ Always include an emoji
.tab-icon; use the default margins · always wrap the.tabsrow in a horizontal-scroll container so the bar scrolls instead of wrapping - Don't
- ✗ Icon-less tabs, SVG icons in tabs, or
style="margin-top:0"per instance (15+ files do this today) · let tabs wrap to a second line (they become unreadable and break the active-underline anchor)
.tabs margin-top:0 override; .tabs used as a toolbar via inline flex (platform-config.js:146); icon-less tabs across most pages; any tab row that wraps on narrow viewports.<div style="overflow-x:auto;white-space:nowrap">
<div class="tabs" style="flex-wrap:nowrap">
<div class="tab active"><span class="tab-icon">🏠</span>Overview</div>
<div class="tab"><span class="tab-icon">📋</span>Detail</div>
<div class="tab"><span class="tab-icon">🕐</span>History</div>
</div>
</div>Eight tabs — shrink the window or zoom in until the bar overflows; it scrolls horizontally instead of wrapping.
Toast
Transient feedback — supersedes the 3 copy-pasted showToast copies and bare alert().
- What
- A
.toastpinned bottom-right; left-border color = severity (.okgreen / default blue /.errred). Auto-dismisses. - When
- Success/info/non-blocking error feedback after an action.
- When not
- Decisions/confirmations → modal. Persistent inline validation → near the field.
- Sizing
- bottom/right 24px · padding 10px 16px · radius 8px · 13px/600 · z-index 1100 · 3px severity left-border.
- Do
- ✓ One shared
showToast()for all transient feedback - Don't
- ✗
alert()/confirm()(~118 calls) or per-page toast copies
showToast (alpha-hunter.js:430, governance.js:258, strategy.js:530) and ~118 raw alert()/confirm() calls across 16 files.Typography
Base font is 14px system sans; numeric/monospace data uses Courier New.
- What
- Body text, headings, and the monospace numeric treatment for data values.
- When
- Numeric KPIs/metrics use the monospace 'value' treatment so digits align in tables and tiles.
- Sizing
- Body 14px/1.5 · section
h214px/600 · stat value 18–24px/700 mono · labels 11px uppercase muted. - Do
- ✓ Use
.value/.s-valuefor headline numbers - Don't
- ✗ Invent ad-hoc
font-size:32pxinline numbers
var(--text).