Normalize radii, drop shadows/blur and font-black, and document the one-view-at-a-time plan in AGENTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.6 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 old MD3-style --color-surface-container-* / --color-on-surface tokens,
the Material Symbols icon font and the Geist/Inter/JetBrains fonts have been
deleted. Never reintroduce them, raw Tailwind palette colors
(slate-*, sky-*, rose-*, white/10...), gradients, or shadows. Use
lucide-react for icons.
Documented exceptions (data-viz only, never chrome): per-service span colors
in the trace waterfall (getSpanColor in otel-utils.ts) and the muted
chart-series hues for metric lines/pods (POD_COLORS).
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 status
Whole UI is on ds-* (one theme, light + dark). The nav strip in Tabs.tsx
is gone - IconRail is the only navigation; Tabs.tsx just renders the
active screen. TelemetryFilters is built on FilterBar. The Material
Symbols icons were replaced with lucide.
Lessons from getting here:
- Merges can silently revert a redesign.
67ce419revertedIssuesVisualizer.tsxto the old styling (a parallel branch had edited the same file). After any merge touchingotel-ui/src, run the greps below - they must return nothing. - A stale
otel-ui/distor un-redeployed image hides changes. The Go server servesotel-ui/dist, else./dist, else the legacy rootindex.html. Rebuild (npm --prefix otel-ui run build) before concluding a screen isn't wired. - Still to polish (functional, but not yet re-laid-out to the screen
pattern):
TraceVisualization,MetricsVisualizer,StructuredLogTerminal,SearchAndWatcher,TrendsVisualizer,SetupPanel. They useds-*colors via a mechanical token swap but keep their old layout/type scale, useonClickon non-buttons in places, andCommandAndQueryBar/WaveformHistogramstill sit above every tab except Issues and RUM sessions (isTriageOverviewinApp.tsx). Restructure them ontoFilterBar+ the card/aside pattern when touching them.
Finding old-pattern usages
grep -rlE "surface-container|text-on-surface|primary-container|bg-error-container|material-symbols-outlined" otel-ui/src
grep -rnE "(bg|text|border|ring|from|to|via)-(slate|zinc|gray|sky|cyan|blue|violet|indigo|purple|emerald|teal|rose|red|amber|orange)-[0-9]|text-white|border-white|bg-(white|black)/" otel-ui/src
Both must print nothing.
Working order from here
Initial pass is done. Take one view at a time to the full screen pattern (FilterBar + card/aside layout, real buttons, fold or restyle the shared chrome for that screen), verify it in the browser in both themes, commit, then move to the next. Suggested order: Traces, Logs, Metrics, Search, Trends, Setup.