11 KiB
Agent instructions
Instructions for AI agents (and contributors) working in this repo. Currently
covers the otel-ui frontend design system, since that's the active area of
work; add sections here as other parts of the repo get their own conventions.
otel-ui: design system
The rule
One theme, light and dark - that's it. otel-ui/src/index.css defines a
single set of --ds-* tokens with a dark variant (default) and a light
variant (:root[data-theme="light"]). Never introduce a second palette, a
third theme variant, or a new token namespace alongside ds-*. If a screen
needs a color, it comes from this table or it doesn't exist yet - extend the
table, don't invent a one-off.
The theme toggle lives in TopBar.tsx, persists to localStorage
(otel.ui.theme), and is applied before first paint by an inline script in
index.html so there's no flash of the wrong theme.
The repo still has a second, older color system - the MD3-style
--color-surface-container-* / --color-on-surface / --color-primary-container
tokens, also in index.css - left over from before this redesign. That system
is dead, not a design choice: it exists only because not every screen has
been migrated yet (see the checklist below). Never add new usages of it. Once
the checklist is empty, delete that whole legacy @theme block plus the old
Geist/Inter/JetBrains/Material-Symbols font <link> entries in index.html,
so exactly one design system remains in the codebase.
Token reference
Source of truth is otel-ui/src/index.css (:root and :root[data-theme="light"]).
Tailwind utilities are generated as bg-ds-*, text-ds-*, border-ds-*.
| Token | Dark | Light | Used for |
|---|---|---|---|
ds-bg |
#0D1014 |
#F4F2ED |
page ground |
ds-chrome |
#0B0E12 |
#FFFFFF |
icon rail, top bar |
ds-surface |
#14181E |
#FFFFFF |
cards, panels |
ds-surface-2 |
#191E26 |
#F7F5F0 |
inputs, chips, selected row |
ds-fill |
#1D232B |
#EDEAE3 |
hairlines inside cards, chart bands, bar tracks |
ds-line |
#242B34 |
#E3E0D8 |
card/control borders |
ds-line-strong |
#2A313B |
#D6D2C8 |
chip borders, neutral badges |
ds-text |
#E8ECF2 |
#161A1F |
primary text |
ds-text-2 |
#98A3B2 |
#4F5964 |
secondary text |
ds-text-3 |
#8B96A6 |
#6B7480 |
labels, metadata, muted text - this is the accessibility floor, don't go dimmer |
ds-critical / ds-critical-bg |
#FF6F5E / #2B1512 |
#C4402C / #FBE9E5 |
breached threshold, open errors |
ds-warn / ds-warn-bg |
#F0B24A / #2A2011 |
#8A5D0E / #F9EFDA |
degraded but not (necessarily) user-visible |
ds-ok |
#4FCF9A |
#1F7A56 |
healthy, resolved, recovered |
ds-accent / ds-accent-bg |
#7B8CFF / #161B33 |
#3B4FC4 / #E9ECFB |
the one interactive accent - primary buttons, selected segment, links, focus |
ds-on-accent |
ds-chrome value |
#FFFFFF |
text on top of ds-accent |
Colour is a signal, not decoration. Chrome, borders and type are neutral.
ds-critical / ds-warn / ds-ok only appear where a real measurement is
out of its expected band or a real state changed (open vs resolved). A
severity pill's color must correspond to an actual state, never picked for
visual variety. Don't tint cards, headers or buttons "for interest."
Fonts
Three families (otel-ui/index.html loads them; index.css exposes them as
--font-display / --font-ui / --font-data, used via Tailwind's
font-[family-name:var(--font-display)] arbitrary syntax):
- Bricolage Grotesque (
--font-display) - page/card headings, big metric numbers. - Instrument Sans (
--font-ui) - all other UI text and body copy. Also the new global default (index.css's@themeoverrides--font-body), so unmigrated screens already inherit this typeface even before their colors move. - IBM Plex Mono (
--font-data) - anything a person would copy or grep: ids,key=valueattribute pairs, timestamps, durations, service names.
Never Inter, Roboto, Arial, or a new font stack.
Layout shell (already built - reuse, don't rebuild)
otel-ui/src/components/layout/IconRail.tsx- 72px fixed icon rail, global nav, onearia-labelper icon.otel-ui/src/components/layout/TopBar.tsx- 58px fixed top bar: search, time range, theme toggle, auth.otel-ui/src/components/FilterBar.tsx- generic filter bar (optional scope segmented control, removable chips, dashed add-filter, an always-visible match readout, clear-all). Reuse this on every screen that lists things instead of building a bespoke filter row.App.tsxwires the shell globally and renders the single page-level<main>. Screens render inside it - see pitfall #2 below.
Screen pattern
Established by IssuesVisualizer.tsx (the triage overview) and
RumSessionsVisualizer.tsx (real-user sessions) - follow this shape for the
remaining screens:
<FilterBar />at the top of the screen's content.- A main content column (
flex-1) plus an optional fixed-width aside (~340-420px) for supporting stats/panels. - Cards:
rounded-[14px] border border-ds-line bg-ds-surface, no shadows, no gradients - depth comes from the border and ground color alone. - No per-service rainbow badges.
getServicePaletteinotel-utils.ts(the gradient hash-to-color picker) is legacy: per the original design spec, a muted per-service palette is reserved for trace-waterfall span coloring only, never for general UI chrome like service badges. Use a neutralborder-ds-line-strong text-ds-text-2badge instead. - Real
<button>/<a href>/<input>+<label>only. NeveronClickon adivorspan[role=button].aria-labelon every icon-only control;aria-hidden="true"on decorative SVG icons (uselucide-react, not the Material Symbols icon font - see pitfall below).
Pitfalls hit migrating Issues and RUM sessions - avoid repeating these
- No nested
<button>elements. Browsers implicitly close the outer<button>the moment they hit an inner one, which silently breaks click handling on whatever's after it in the DOM (this shipped once, undetected until manual testing). If a row/card needs both "click to expand" and inner action buttons, make the expandable area its own<button>and put the action buttons as siblings, not children. - No nested
<main>landmarks.App.tsxalready renders one<main>; a screen component must not render its own - use a<div>. whitespace-nowrap+shrink-0on fixed-height pills/badges. Without them, a longer label silently wraps onto two lines and blows out the pill's height inside its flex row - easy to miss until you test with real (longer) data or a narrower viewport.- Never fabricate data. Before adding a metric, column, or button, check
whether the field actually exists in the real data model - the backend API
response, or for RUM specifically, what
static/otel-rum.jsactually sends (see the header comment inotel-ui/src/lib/rum-utils.tsfor a worked example of what's real vs. invented). If a design reference shows something with no backing data - the RUM mockup invented rage clicks, geo location, a "frustration score", and session replay, none of which this pipeline captures - drop it or replace it with the closest real, honestly-labeled equivalent. Never wire up a control that looks functional but isn't backed by anything. - Screenshots of a hidden Browser preview pane are unreliable for
position: fixedelements (stale compositor frame, even after a full reload). If a screenshot shows something that contradictsgetComputedStyle, trust the computed style, front the tab, and re-check before concluding there's a real bug.
Migration checklist
Converted to ds-* already: App.tsx (shell), IconRail.tsx, TopBar.tsx,
FilterBar.tsx, IssuesVisualizer.tsx (Issues / triage overview, tab0),
RumSessionsVisualizer.tsx (RUM sessions, tab7).
Everything below still renders with the legacy MD3 dark-only palette
(text-on-surface, bg-surface-container-*, bg-primary-container,
gradient badges, the Material Symbols icon font) and needs the same
treatment. Sizes are current line counts, biggest first:
| File | Lines | Tab / role | Notes |
|---|---|---|---|
TraceVisualization.tsx |
1866 | Traces & Spans (tab1) | Waterfall + span detail; closest existing screen to the design spec's trace-waterfall.html reference |
MetricsVisualizer.tsx |
1107 | Metrics & APM (tab2) | Imports GaugeMeter.tsx |
StructuredLogTerminal.tsx |
582 | Live Log Stream (tab3) | Uses Material Symbols icons |
Tabs.tsx (shell) |
513 | horizontal tab strip rendered above every screen | Gradient active-tab underline, slate colors; decide whether this strip is still needed now that IconRail is the primary nav, or whether it should just lose the gradient/slate styling |
SearchAndWatcher.tsx |
504 | Archive & Trap (tab6) | |
ServiceGraph.tsx |
495 | used by TraceVisualization.tsx |
migrate alongside its caller |
WaveformHistogram.tsx |
385 | shown above every non-Issues/RUM tab's content | Uses Material Symbols icons; see the shared-chrome note below |
GaugeMeter.tsx |
372 | used by MetricsVisualizer.tsx |
migrate alongside its caller |
TrendsVisualizer.tsx |
323 | Historical Trends (tab5, only when ClickHouse stats are configured) | |
CommandAndQueryBar.tsx |
251 | shown above every non-Issues/RUM tab's content | see the shared-chrome note below |
TelemetryFilters.tsx |
124 | conditionally shown above Tabs.tsx content |
service multiselect + attribute exclusions; candidate to rebuild on top of FilterBar.tsx instead of restyling in place |
SetupPanel.tsx |
112 | Setup & Agent API (tab4) | |
PatternsVisualizer.tsx |
76 | Similarity Patterns (tab8) | |
ViewErrorBoundary.tsx |
62 | error fallback UI, wraps every tab's content | small, easy pass |
RolloutsVisualizer.tsx |
48 | Cluster Rollouts (tab9) | |
AuthCallbackView (inside App.tsx) |
~40 | OIDC callback screen | still 100% old tokens even though the rest of App.tsx is migrated |
Shared chrome note: CommandAndQueryBar.tsx (anomaly banner, filter
status strip, NDJSON export) and WaveformHistogram.tsx (volume/error
waveform with click-to-focus) are currently skipped entirely on the Issues
tab (isTriageOverview branch in App.tsx) because their old styling would
clash with the new FilterBar. When migrating the remaining tabs, decide
per screen whether that real functionality gets folded into the screen's own
FilterBar/content area (as happened for Issues and RUM sessions), or stays
as shared chrome above the remaining tabs, restyled to ds-* tokens.
Finding old-pattern usages
grep -rlE "surface-container|text-on-surface|primary-container|bg-error-container|material-symbols-outlined" otel-ui/src
grep -rlE "from-[a-z]+-[0-9]+ via-[a-z]+-[0-9]+|getServicePalette" otel-ui/src
A file with zero hits and zero ds-* usage either has no meaningful styling
of its own (a pure hook/context) or hasn't been looked at yet - check the
checklist above.