📐 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).