Overview
Imbi is a DevOps Service Management Platform for managing large environments containing many services and applications. It provides a centralized service catalog with metadata management, dependency tracking, ownership hierarchy, and AI-powered features. The UI is a utility-forward, high-density tool: warm paper surfaces, a single amber brand voice, muted semantic chips, and type chosen for reading long lists of service names all day.
Stack: React 18 · TypeScript · Vite · Tailwind · shadcn/ui primitives (Radix) · React Query · React Router v7. Type: Inter / JetBrains Mono. Brand: WorkerBee amber.
Tokens are defined once in src/index.css as CSS custom
properties and exposed through Tailwind utility classes via
tailwind.config.js. Dark mode is class-based (.dark
on <html>). A pre-React bootstrap in
index.html applies the saved theme before first paint
to avoid a flash.
Principles
Four rules that set the line between cleaner and busier. When in doubt, remove.
- One brand voice, many muted helpers. Amber is the only saturated color. Status and environment chips sit on quiet tinted backgrounds so a card reads as one object, not five.
- Density over decoration. 142 services want tight rows and small type, not generous gradients. Pad where type breathes; compress where the eye scans.
- Warm grays, not cold. Ink and paper are hue-shifted toward amber (~40° hue). Never pure black on pure white.
- Tokens before exceptions. If you reach for a raw hex in a component, it belongs in the token file. Hardcoded colors are the #1 source of theme drift — the tokens migration in commit
4cc2e78exists to enforce this.
Color
Surface ramp is warm paper in light, warm ink in dark. Both share the same amber accent so the brand does not shift.
Surfaces & ink
| Token | Light | Dark | Use |
|---|---|---|---|
| --bg-app | #f5f4f0 | #141413 | page canvas |
| --bg-surface | #ffffff | #1e1e1c | cards, panels, nav |
| --bg-sunken | #eeedea | #2a2a27 | toolbars, inputs-in-cards |
| --fg-primary | #1a1a18 | #e8e7e3 | titles, body |
| --fg-secondary | #5f5e5a | #a0a09a | descriptions, labels |
| --fg-tertiary | #888780 | #6b6b65 | meta, captions |
| --border-subtle | rgba(0,0,0,.15) | rgba(255,255,255,.15) | cards, dividers |
| --border-default | rgba(0,0,0,.3) | rgba(255,255,255,.25) | buttons, inputs |
| --border-strong | rgba(0,0,0,.4) | rgba(255,255,255,.35) | hover / active |
Brand — Amber
Single ramp from --amber-50 to --amber-800. The primary action is --action-bg which flips between #b8860b (light) and #fbce39 (dark) to match the header logo in each theme.
Semantic palette
Six roles. Each exposes a bg, fg, and
dot. Backgrounds read at ≥7:1 against their fg; dots are
used for filter pills and chip leaders. Defined in
src/index.css and mapped to Tailwind utilities as
bg-{role} / text-{role} / border-{role}.
| Role | Purpose | bg (light) | fg (light) | Tailwind |
|---|---|---|---|---|
| Success | confirmed state, passing checks | #eaf3de | #27500a | bg-success / text-success |
| Warning | needs feedback, pending | #faeeda | #7c5900 | bg-warning / text-warning |
| Danger | error, failed, blocked | #fcebeb | #a32d2d | bg-danger / text-danger |
| Info | neutral context, related link | #e6f1fb | #185fa5 | bg-info / text-info |
| Accent | implementing, follow-up | #eeedfe | #3c3489 | bg-accent / text-accent |
| Neutral | queued, default | #f1efe8 | #444441 | bg-tertiary / text-secondary |
Example: environments
Environments are user-configurable (see Label colors) — Testing / Staging / Production are instances of the Label system with default seed colors, not a parallel semantic role.
EnvironmentBadge (src/components/ui/environment-badge.tsx).Label colors
Environments, blueprints, teams — anywhere the product lets a user tag an object with a color — share a single 8-swatch palette. Saturation is capped at ~60 and lightness at ~58 so every swatch sits on the same tonal plane as the amber brand and warm paper surface.
The palette
| Name | Hex | Seeded use | Chip preview |
|---|
Chip derivation
Chips compute their own bg and fg from the
stored hex. Users never pick fg — the system guarantees contrast.
// Light mode (matches ColorPicker preview + EnvironmentBadge style) bg = rgba(r, g, b, 0.20) // 20% alpha of swatch fg = darken(hex, 0.35) // multiply channels by 0.65 // Dark mode fg = lighten(hex, 0.30) // shift toward white
Rules
- Never present the raw hex picker as the first interaction — the 8 swatches cover 95% of cases and enforce the system.
- The free-form hex input is an escape hatch. When a user types a custom hex, clamp saturation to ≤65 silently before saving.
- Semantic colors (success, danger, info, warning) are not in this palette. Those are reserved for system messaging.
- A label's color is stored as raw hex, not a palette index — so adjusting defaults later won't repaint existing data.
- The picker is defined once in
src/components/ui/color-picker.tsxand reused anywhere a color is captured.
Typography
Inter for UI, JetBrains Mono for IDs, hashes, and counts. The scale
is short on purpose — every size below is defined in
tailwind.config.js under fontSize.
| Utility | Size | Use |
|---|---|---|
| text-h1 | 24px / 500 | Page title |
| text-h2 | 20px / 500 | Section title |
| text-card-title | 18px / 500 | Card heading |
| text-task | 16px / 500 | Task / inline heading |
| text-base | 15.5px / 400 | Body |
| text-sm | 15px / 400 | Secondary body |
| text-badge | 13.5px / 500 | Badge text |
| text-xs | 13.5px / 400 | Meta |
| text-chip | 13px / 400 | Chip label |
| text-overline | 13px / 500 · 0.5 tracking | Section eyebrow |
Spacing
Page and component spacing is centralized in
tailwind.config.js under theme.extend.spacing
and applied with named utilities instead of raw numbers.
| Token | Value | Use |
|---|---|---|
| nav-height | 48px | Top navigation bar |
| page-x / page-y | 20 / 28px | Page edge padding |
| card-x / card-y | 18 / 14px | List card padding |
| card-detail-x / -y | 22 / 18px | Detail card padding |
| tab-x / tab-y | 18 / 11px | Tab button padding |
| tab-panel | 22px | Tab content padding |
| form-gap | 16px | Gap between form fields |
| meta-gap | 8px | Meta row inline gap |
| avatar-gap | 12px | Avatar + name gap |
| card-gap | 8px | Chips inside a card |
Radii & elevation
Soft but not squishy. Tailwind's rounded-{sm,md,lg,full}
map to 4 / 8 / 12 / 9999 px respectively.
sm for chips, md for controls,
lg for cards and panels.
| Token | Value | Use |
|---|---|---|
| rounded-sm · --radius-sm | 4px | chips, badges |
| rounded-md · --radius-md | 8px | buttons, inputs |
| rounded-lg · --radius-lg | 12px | cards, dialogs, panels |
| rounded-full | 9999px | pills, avatars, health |
Elevation
| Token | Use |
|---|---|
| --shadow-sm | card hover |
| --shadow-md | popovers, menus, dropdowns |
| --shadow-lg | modals, dialogs |
| --ring | focus ring (amber @ 25% alpha) |
Inputs
Single field style. Implementation in
src/components/ui/input.tsx and
src/components/ui/textarea.tsx. Focus uses the amber
ring; error states swap the border to border-danger.
Chips & badges
One shape, six semantic colorways plus three named environment colorways. If you need a new state, extend the semantic set — no custom shapes.
Semantic
Sizes
Health indicator
Tinted background + 1.5px ghost ring, not a heavy
ring-4. Thresholds: ≥85 ok,
75–84 warn, <75 bad.
Cards
One card shell — 12px radius, 1px subtle border, card padding tokens.
Implementation: src/components/ui/card.tsx (shadcn).
Composable with CardHeader / CardContent /
CardFooter.
Tabs
Single-row tabs for object-level navigation (Overview, Configuration, Dependencies, …). Active state is a 2px amber underline, not a filled pill — filled pills are reserved for the top nav.
Never bold weight + underline on the same tab — one cue is enough. Count pills flip to amber on the active tab.
Version chip train
Deploy state across environments in one row. Chips are grouped inside a sunken surface, separated by right-arrows to imply promotion flow. Hashes use JetBrains Mono with tabular-nums so they don't jiggle on updates.
Key/value rows
Spine of every detail view. Label on the left in
fg-tertiary, value right-aligned in primary ink. Rows
separate with border-subtle, never alternating
backgrounds. Implementation: src/components/EditableKeyValueCard.tsx
and src/components/ui/key-value-editor.tsx.
Segmented & filters
Segmented controls for mutually-exclusive views. Filter pills for additive narrowing. Never mix them in the same row.
Segmented
Filter pills
Color picker
Used anywhere a user labels an object with a color — Environments,
Teams, Blueprints, Project Types, anything tagged. Renders the
8-swatch label palette as the primary path, custom hex as an escape
hatch, and a live preview rendered at the three sizes the chip
appears across the product. Implementation:
src/components/ui/color-picker.tsx.
API
<ColorPicker value={labelColor} // controlled hex, "" when unset onChange={setLabelColor} // fires on swatch / hex / native picker objectType="environment" // "environment" | "project type" | ... labelValue={name} // name used in preview chips />
Anatomy
- Swatch grid ·
role="radiogroup", 8 radio buttons on one row. Primary path. Selected swatch shows dark border + white check. - Name row · shared vocabulary — users and docs refer to labels by name ("Moss"), not hex.
- Divider · or custom hex marks the escape hatch as secondary.
- Hex input · native color-picker pip on the left (OS eyedropper), editable hex, contrast status on the right.
- Preview · rendered inside the picker using
labelValueat three sizes (22px chip, 26px chip, dot+text).
Rules
- Contrast status flips to "Low contrast" (warning) when chip bg/fg fails 3:1. Does not block save — the user is the tenant admin and may have reasons.
- Keyboard:
ArrowLeft/Right/Up/Downcycle focus,Space/Enterselect,Tabjumps to the hex field. tabindexis roving (only the selected swatch is in the tab order).- The label/name text in the preview mirrors the adjacent Name field in the form; the picker does not fabricate it.
Token reference
All surfaces, ink, and semantic colors are defined as CSS custom
properties in src/index.css and mirrored into Tailwind
utilities via tailwind.config.js.
/* Surfaces */ --color-background-primary → bg-primary · cards, panels --color-background-secondary → bg-secondary · secondary surface --color-background-tertiary → bg-tertiary · page canvas / sunken /* Ink */ --color-text-primary → text-primary · titles, body --color-text-secondary → text-secondary · descriptions --color-text-tertiary → text-tertiary · meta /* Borders */ --color-border-tertiary → border-tertiary · default divider --color-border-secondary → border-secondary --color-border-primary → border-primary · hover / active /* Brand / action */ --color-action-bg → bg-action · primary CTA (#b8860b / #fbce39) --color-action-bg-hover → bg-action-hover --color-action-fg → text-action-foreground /* Semantic triples — bg / fg / border */ --color-background-{info,success,warning,danger} --color-text-{info,success,warning,danger} --color-border-{info,success,warning,danger} // Utilities: bg-info / text-info / border-info (etc) // Ring utilities: ring-info / ring-success / ... /* Shadcn compatibility (HSL triplets) */ --background / --foreground --primary / --primary-foreground --secondary / --muted / --accent / --destructive / --card / --popover --border / --input / --ring
Chart and status palettes (task status, link type, avatar fallback)
are defined as hardcoded Tailwind color objects inside
tailwind.config.js rather than CSS variables because
they are not theme-reactive.
Codebase map
When implementing a feature that touches the design system, these are the files to read first.
| Path | What lives there |
|---|---|
| src/index.css | All CSS custom properties; :root (light) + .dark overrides. |
| tailwind.config.js | Utility mappings (bg-*, text-*, border-*), typography scale, spacing tokens, status color objects. |
| index.html | Pre-React bootstrap that applies the saved theme class before first paint. |
| src/contexts/ThemeContext.tsx | Theme provider + useTheme() hook. Uses useLayoutEffect to keep the class in sync. |
| src/components/ui/ | Shadcn-based primitives: button, input, card, dialog, tooltip, admin-table, environment-badge, color-picker, key-value-editor, icon-picker, … |
| src/components/ui/environment-badge.tsx | Renders the Testing/Staging/Production chip from a stored label_color hex (20% alpha bg, 40% alpha border, raw hex fg). |
| src/components/ui/color-picker.tsx | The 8-swatch picker; takes value, onChange, objectType, labelValue. |
| src/components/admin/environments/EnvironmentForm.tsx | Canonical consumer of ColorPicker (passes objectType="environment" and labelValue={name}). |
When to reach for which token
- Prefer semantic utilities.
text-danger,bg-success,border-info— nevertext-red-500. - Prefer named spacing.
px-page-x,py-card-yover raw Tailwindpx-5when the context is a known surface. - Prefer
rounded-{sm,md,lg}over raw pixel values — the mapping is defined once. - Prefer the theme context. Read
isDarkModefromuseTheme()instead of inspectingdocument.documentElement.classList. - Never hardcode a hex inside a component. Add it to
src/index.css(ortailwind.config.jsfor status/chart palettes) and expose a utility.