Primitives, surfaces and tokens. The spec is DESIGN.md.
Semantic tokens — OKLCH color space, light & dark mode
Surfaces
background
card
light-muted
muted
heavy-muted
Brand
primary
primary-light
secondary
secondary-light
accent
Status
success
success-light
warning
warning-light
queued
queued-light
destructive
destructive-light
Two voices — InterVariable for UI, Freight Big Pro for brand & editorial. IBM Plex Mono for coordinates.
Voice 1 · UI · Inter
The operator's console.
Dense UI — tables, forms, buttons, labels. ~95% of the platform and all landing supporting copy.
The quick brown fox jumps over the lazy dog. 1,284 executions completed in 3m 42s average.
Voice 2 · Brand & Editorial · Freight Big Pro
Build agents that run the web.
Landing hero & section titles. In-product hero moments, empty states, onboarding, quotes. One expressive voice across both surfaces.
“A polished power tool that doesn't intimidate.”
Coordinate · IBM Plex Mono
Not a voice — a coordinate system. Identifiers, versions, model slugs, metadata k:v pairs, cron expressions, URLs & paths, raw enum values, and tabular numerics. Where precision beats emotion. Use <Mono> from @workspace/ui/components/mono outside of DataTable (which has meta.mono).
Mono
Not mono
Type roles: pick the role, not a size
Editorial tier — Freight Big Pro (use sparingly)
In-context example — empty state
Executions
Nothing to see here — yet.
Kick off your first workflow run and this page will fill up with the interesting stuff.
Serif headline + Inter supporting copy + mono overline. The three voices playing together.
Button face is lifted from flow, shadow at flow position — sinks on hover, presses on active
Variants × Sizes
| variant \\ size | sm | default | lg |
|---|---|---|---|
| default | |||
| destructive | |||
| outline | |||
| ghost | |||
| link |
With Icons
Icon Only
States
Common Patterns
Dialog Footer — confirm / cancel
Destructive Confirmation
Toolbar — mixed actions
Tab Toggle — ButtonGroup variant switching
Page Header — primary + secondary actions
Split Button — primary action + dropdown
Icons are a supporting voice — they reinforce an affordance or classify a type. Four working sizes, six semantic colors, and a strict anti-pattern list. See DESIGN.md §Iconographyfor the canon.
Time components never wear leading icons — the mono typography carries the signal. Cross-link: §Time & Duration.
1 · Size scale
Four working sizes carry 99% of the system. size-3 is only for tiny glyphs inside chips, pills and the drawer Next pill. size-6 is reserved for rare page-header chrome.
size-312pxsize-3.514pxsize-416pxsize-520pxsize-624pxsize-832px2 · In buttons
Button hands every nested icon size-3.5 and gap-1.5. Never pass size-* or mr-* class overrides.
3 · In tabs
TabsTrigger size="default" and ChromeTab use size-3.5. size="lg" steps up to size-4 because the tab IS the page title.
4 · In menus
DropdownMenuItem and CommandItem both hand icons size-4 and gap-2. Delete rows use text-destructive; everything else inherits text-muted-foreground from the primitive.
5 · In entity and stat cards
Entity full tile (size-10 rounded-lg) holds size-5; compact (size-7 rounded-md) holds size-3.5. A stacked drawer header uses the same tile as the full card. Stat card header icon is size-3.5, right-aligned in the muted label row.
6 · Time components never wear icons
Mono typography + tabular-nums + the two-line tooltip carry the signal. The column header / row label / prefix prop name the field.
Bare time component, or prefix prop for the label. Mono font does the work.
Clock/Calendar/Timer prefixing a time component. The column header already says 'Last run'.
7 · Color canon
Icons inherit currentColor. Use semantic tokens. Never text-green-600, text-red-500, and friends.
text-successtext-destructivetext-warningtext-primarytext-admintext-muted-foreground8 · Empty state hero
EmptyMedia variant="icon" renders a size-10 muted tile around a size-8 icon. Do not pass a size class on the icon itself.
9 · Inputs with a leading icon
No built-in SearchInput primitive yet — use an absolutesize-3.5 Search icon with muted foreground and pad the input to match.
10 · Anti-patterns
Four classes of drift the canon forbids. See §Iconography for the full list.
Status badge tint IS the signal — one glyph or none.
Status-tinted badge AND a same-meaning status glyph next to it.
Let the Button primitive supply size-3.5 and gap-1.5.
Overriding size-4 fights the canon. The Button no longer uses size-3 by default — trust it.
Semantic tokens: text-success, text-destructive, text-warning, text-primary, text-admin.
Raw palette colors. No text-green-*, no text-red-*, no hex literals.
One size per visual group — size-3.5 here.
Mixed size-3 and size-4 in the same row. Pick one.
11 · Admin affordance
The element IS the signal — amber button / amber text instead of an extra badge. AdminBadge scales its Shield glyph (xs: size-2.5, sm: size-3, md: size-3.5).
12 · Loaders
Loader2 animate-spin in a status position inherits the surrounding size canon. Keep it the same size as the icon it replaces.
Grouped actions with shared offset shadow
Horizontal — outline
Horizontal — alignment
Vertical
With Text & Separator
Variant groups
Tinted backgrounds with colored text — quiet labels, not loud fills
Variants
Run status · StatusBadge with the run-status map
Tint and label carry the state. The only glyph is the live dot on states where work is happening now.
The affordance IS the signal — admin elements paint themselves amber. Badge stays as the page-header chip and the fallback for elements that already carry color. See DESIGN.md → Admin Indicators.
Token
--adminSolid amber. Use on raised admin buttons; rarely as a surface.
bg-admin/10Tinted background — strip, hover/active states, ghost surface.
border-admin/30Border tint for the strip and outlined admin elements.
Element coloring (primary pattern)
Buttons
Sidebar items
Properties & links
Form-control primitives (admin prop)
<Input admin /><SelectTrigger admin />Playwright traces for every session.
<FeatureToggle admin /> (threads to <Switch admin />)Controls inside <AdminOnly> accept an admin prop instead of hand-rolling amber classes. The palette, focus ring, placeholder tone, and switch track all flip together. Size and disabled behavior are unchanged.
AdminBadge — fallback & page-header
size="xs" — fallback onlysize="sm" — raresize="md" — page headerThree contexts
Property
Element coloring is the primary signal — render the value in text-admin.
Control
The button paints itself amber. No badge needed.
Page header
One md badge marks the whole page identity.
Admin mode toggle (live demo)
AdminPageGate (toggle-off interstitial)
You turned Admin mode off — internal tools are hidden so it's safe to demo. Re-enable to view this page.
Anti-patterns
Don't bolt a badge onto every admin element
The element's own admin coloring is the signal. Use variant="admin", not <AdminBadge /> stapled next to a default-styled control.
Don't use --admin for warnings
Status colors stay status colors. For "might be dangerous," reach for --warning or --destructive. Admin is a category, not a status.
Don't gate access with color or the badge
Both are signals for admins, not gates. Server-side isAdminUser() route guards still enforce real access.
Don't show admin elements to non-admins
Hide the entire element via <AdminOnly>. Disabled admin features leak product surface.
Chrome-style tabs — active tab uses bg-background against a muted bar surface, with inverted SVG corners
Tabs (Base UI) — sub-navigation within a page or resource (Overview / Analytics / Reports). Self-contained, keyboard-accessible, internal state.ChromeTabs — browser-style document tabs (the agent workspace: builder, executions, open execution tabs). Controlled, supports drag-to-reorder, close, sticky pins, add-dropdown.Default framed look — Tabs / ChromeTabs
The canonical chrome-tab visual: transparent strip, active tab with border + stroked inverted corners, content panel with full border + rounded-b-lg, and one shared drop-shadow over the whole tongue-card silhouette. Both Tabs and ChromeTabs default to framed; the panel border auto-paints so consumers don't repeat border border-t-0 rounded-b-lg. Suppress the drop-shadow when the strip and workspace can't share a DOM subtree (agent header pattern) via framedShadow={false}.
Tabs framed (sub-navigation, on bg-light-muted)
Border traces the active tab via the inverted corners and wraps the whole content card. One shared drop-shadow.
ChromeTabs framed (one shared drop-shadow)
Active: exec-92890146 — ChromeTabs wraps strip + workspace and applies the shared drop-shadow over the whole tongue-card silhouette when framed.
Flat variant — framed={false} on inline tabs
Opt out via framed={false} for tabs that live inside an existing card, modal, or section, where a bordered tongue card around the content would double up. The strip gains a bg-muted plate; the panel is opinion-free (consumer decides shape).
With icons
Executions content
Text only
Overview content
With badges
All logs
With disabled tab
Active content
Browser-style tabs — ChromeTabs
Active: exec-92890146 — Drag non-sticky tabs to reorder, close with the X button (or middle-click), open more from the + menu. The first three tabs are sticky and cannot be moved or closed.
Custom layouts — ChromeTab standalone
The same visual, used as a standalone primitive. Reach for this only when neither Tabs nor ChromeTabs fits — e.g. a custom router integration that needs full control over rendering.
Standalone ChromeTab (manual layout)
Content for: executions
On light-muted surface
Executions — uses bg-light-muted for the tab bar, matching the agent header.
Minimal
Users content
Full width
All items
A form is sections of fields; a field is one label and one control. FormField encodes the anatomy and the aria wiring. Type roles replace ad-hoc sizes and weights. See DESIGN.md §Forms and §Type roles.
Field states
We POST each event here.
Admin only.
Placeholders
FormSections · FormSection · FormField
Label, optional marker and info tip share one row. The description sits between label and control, so the error stays next to the value. Try a concurrency above 50.
Drawers, the graph inspector and large dialogs share one panel anatomy. Drawers stack numbered sections. Status uses one badge with a map per domain. Pick the surface with DESIGN.md §Surfaces.
The rules · first match wins
Is the user confirming something destructive, irreversible or costly?
Is the system showing a value the user can see only once?
Is the user changing exactly one value already on screen?
Is the user in a workspace, configuring the object they selected?
Is the user creating, viewing or editing a configuration entity?
Is it a one-screen task that is not the page's main job?
Nothing above matched.
The seven surfaces
| Surface | Blocks | URL | Commits |
|---|---|---|---|
| Inline edit | No | No | Enter, blur or change |
| Popover | No | No | Change or Apply |
| Alert dialog | Yes | No | One verb |
| Dialog | Yes | No | One verb |
| Drawer | Yes | ?new= ?id= §ion= &edit=1 | One save bar, drafts kept |
| Docked panel | No | Selection | Draft, Publish commits |
| Route | — | Yes | Per section |
One blocking layer
Only an alert dialog may open over a Dialog or drawer, to confirm a destructive action inside it. Creating a dependency slides in within the drawer section, or swaps a Dialog's body. Menus and popovers are not layers.
Entity assignments
| Entity | View | Create | Edit | Destroy | Today |
|---|---|---|---|---|---|
| Workflow | Route | Route /create | Inline name, route tabs | Alert dialog | OK |
| Workflow node | Docked panel | Graph menu | Docked panel | Alert dialog | Migrates |
| Execution | Route | Run: Dialog lg | — | Alert dialog | OK |
| Schedule | Drawer | Drawer | Drawer | Alert dialog | Migrates |
| Batch | Route | Route wizard | — | Alert dialog | OK |
| Profile, profile group | Drawer | Drawer | Drawer | Alert dialog | OK |
| Secret | Profile section | Slides in a section | Slides in a section | Alert dialog | OK |
| Secret template | Drawer | Drawer | Drawer | Alert dialog | OK |
| Environment | Drawer | Drawer | Drawer | Alert dialog | OK |
| Integration | Drawer | Drawer | Drawer | Alert dialog | Migrates |
| API key | List | Drawer | — | Alert dialog | Migrates |
| Org member | List | Drawer | Inline role | Alert dialog | Migrates |
| Tag | Popover | Inline | Inline | Alert dialog | Migrates |
| ClassPass venue | Route | Drawer | Route tabs | Alert dialog | Migrates |
Full table with the extra actions (run, retry, show once) in DESIGN.md §Surfaces → Entity assignments.
Docked panel · graph inspector
PanelHeader with back, type overline, docs link and an inline editable name. Hover the name to edit; Enter commits, Escape reverts.
Drawer · numbered sections, edit mode
One list, one section open at a time. Header Edit turns every section into its form; one save bar. Create uses the same sections, with a Next pill on the divider below the open section.
Drawer · view
Same header grammar as the inspector, inside a right sheet.
Steps · routes only
For route wizards such as Batch upload. Completed steps are clickable; upcoming ones are not. Never inside a drawer.
DetailGrid · hint, info, description
Hint: a few words on the value, or why a control is disabled. Info: what the field means, behind the ?. Description: the rare line a user must read. A boolean shows its state dot before the key. A control replaces the value through trailing; units sit beside it.
StatusBadge · status map
One map per domain. Only live states carry a glyph. Unknown values render neutral, humanised.
Empty · size sm
Concept icons
Import from @workspace/ui/lib/icon-concepts. Entity glyphs (for example profile) live in the platform entity registry.
The complete R5 drawer for one entity, built like the Logins page in the product. Create, view and edit share four numbered sections. Edit in the header edits them all behind one save bar. The address bar shows the URL each mode writes.
Modes and URLs
| Mode | URL | Try it |
|---|---|---|
| Create | ?new=profile§ion=… | Create a login profile. Numbered sections. A Next pill on the divider below the open section opens the next one. Open any section directly. No Review step. |
| Validate | — | Clear the name and open another section: Details turns red. Create opens the first section with a problem. |
| View | ?id=…§ion=… | Click a row. Every section reads as a summary. One section is open at a time. |
| Edit | …&edit=1 | Edit in the header turns every section into its form. One save bar at the bottom. |
| Instant section | — | Secrets act at once, in view and edit. New secret slides in over the list and slides back when done. |
| Create-only field | — | Sticky IP is set at create. Afterwards its section shows it read-only with the reason. |
| Destroy | — | Delete lives in the header menu, hidden while editing. It confirms with an alert dialog over the drawer. |
Same sections, every mode
| # | Section | Saves |
|---|---|---|
| 1 | Details / Overview | Draft |
| 2 | Secrets | Instant |
| 3 | Email inbox | Draft |
| 4 | Sticky IP | Create-only; read-only after |
The rules applied to real entities. Each frame is a working slice of the platform built only from the primitives above. Sheets, dialogs and alert dialogs open over the whole page, as they do in the product.
Select a node; the panel follows. Rename it in the header and watch the canvas. The folder button swaps the panel body to files. New environment opens the environment drawer in create mode and selects the result.
How to build a Dialog once the rules chose one (R1 confirms, R2 show once, R6 tasks). One shell, four sizes set by content: sm 0–1 input, md 2–8 fields, lg 9–16 fields in 2–4 sections or tabs, full files, diff and debugger only. See DESIGN.md §Modals.
Alert dialog · R1
sm · 448Destructive, irreversible or costly. Title asks the question, body states the blast radius, the action names the verb. Cancel keeps the thing.
Task · R6
md · 672A one-screen task that is not the page's job. 2–8 fields, one section, no scroll. FormField runs the body; Cancel + verb in the footer.
Tabbed task · R6
lg · 896An R6 task with 2–4 sections: header, persistent identity strip, chrome tabs, body, footer. Tasks only. Entities open in their drawer (R5), never in a tabbed dialog.
List-pick
md · 672Search + scroll + multi-select inside a DialogBody. The toolbar pins to the top, the list scrolls, the count summary lives in the footer.
Loading + error
md · 672Same shell, two states. Loading uses skeleton inside the body. Blocking errors are an in-body banner — never a toast.
Right-action + split footer
lg · 896The Run workflow shape: Pre-fill Select sits in the header right-action slot; Schedule and Advanced cluster left in the footer; Cancel + Run cluster right.
Size by content
Anything else (sm:max-w-[600px], sm:max-w-5xl, custom max-h-[Nvh]) is a smell.
Compact metric tiles — label + icon, value, helper. Used on /overview and /executions and reserved for future summary strips (billing, usage, scheduled). One row, 3 or 4 tiles, top of page.
Anatomy
Strip — 4 tiles · /overview
Strip — 3 tiles · /executions
Loading — full strip skeleton
Future use — billing strip (same shell, different content)
Same shell, swap the helper. Numeric metric → number value + breakdown helper. Set of related items → no value, badge children. Trend tile → same shell, badge as the helper (see kitchen-sink dashboard below).
How a domain entity (Profile, Integration, Notification, API Key, Schedule, …) renders anywhere in the platform. The same identity — icon, name, type, meta — at three densities: full on index pages, compact in side panels and drawers, and chip as an inline reference with a popover. Detail lives in a vertical stack of named body sections — and only those sections — so two cards for different entity types still read as members of the same family.
Anatomy — full variant
EntityCardType) — small uppercased mono label sitting above the title. The icon already telegraphs the type via shape; the overline is the belt-and-braces text label.text-base font-semibold, truncating. The dominant element of the header now that identity info is split across overline + title only.space-y-4) of named section primitives. This card uses PropertyList + FilterRow.EntityCardTimestamps with showCreated renders Created always and Edited when changed (60s threshold). Empty footers don't render at all.Variants — full / compact / chip (hover the chip)
Full
Compact
Chip + popover
Full
Compact
Chip + popover
Body grammar & footer — six body sections plus the audit footer
Every full or popover body is a stack of named sections. Picking from this set is mandatory; ad-hoc divs in an entity body are out. Two densities of PropertyList for key/value, four cluster sections for everything else.
PropertyList density="grid"
PropertyList density="inline"
PillRow
DividedList
DividedList (editable, with actionSlot)
FilterRow (with overflow)
EmptyRow
EntityCardFooter (default)
Edited >60s after create — meta already shows Created.
EntityCardFooter (showCreated)
Custom-meta entity (e.g. Schedule) — footer carries Created too.
Composed example — three sections in one body
The same shell with a stacked body. Sections sit at space-y-4; headers and content within a section sit at space-y-2.
Chip references — hover or click to expand
Chips are entity references inside other content (table cells, log lines, toasts, breadcrumbs). Identity only — icon, type, name. The popover shows the entity's compact card plus an allow-listed subset of body sections, and a footer link that opens the entity in a new tab.
Two underlying surface modes — canvas(used by Plain, Hero, and Start) and chrome(used by Tabbed and Detail) — produce the platform page shapes. Canvas pages get a bg-light-muted well with bg-card surfaces lifting off it. Chrome pages drop the canvas and let a bg-sidebar strip + tabs sit directly above a single bg-background content panel — the active tab is the only seam between chrome and content. Sidebar carries the page label; the title slot is reserved for the active tab (Tabbed/Detail), the editorial composition (Hero/Start), or nothing at all (Plain).
Anatomy — canvas mode vs chrome mode
Canvas mode · Plain & Hero
Three surfaces: chrome → canvas → cards. Page bar is transparent on the canvas, optional, slim. Cards lift off the canvas via border + ~1.5% lightness delta.
Chrome mode · Tabbed & Detail
Two surfaces: chrome → content panel. Tabs sit onbg-sidebar; the active tab is the only seam between chrome and content. Actions live inside the tab content because they're tab-scoped.
TabsListoverride!bg-transparent !px-0 so the strip itself is the bar (no nested-bar effect)TabsContent (they're tab-scoped)The core shell shapes
A · Plain
Top-level routes with one purpose · sidebar carries the label
No title. The sidebar already says "API Keys" — repeating it in a header strip is wasted real estate. Page bar carries only the primary action; everything else is the content card.
B · Tabbed
Chrome strip + tabs · no canvas layer · tab is the only seam
No canvas layer. The page chrome (bg-sidebar) extends up over the tabs; the active tab is the only seam between chrome and the white content panel below. Help toggle + Create button move into a tab-scoped action row at the top of each tab content — they swap with the active tab anyway. Strip stays pure navigation.
C · Hero
Dashboard / summary routes · editorial moment replaces the title
No page bar at all. The editorial hero is the first thing you see — a serif headline that adapts to state, mono overline for context, Inter copy with live numbers. Stat strip and content cards stack below.
D · Detail
Resource pages · custom header carries the resource name
Custom resource header. The page bar grows up onto the chrome (bg-sidebar) and carries: workflow selector (resource title) → ChromeTabs (sticky views + open documents) → primary action (Run). Detail pages may opt into surface="flat" when the canvas IS the application (Builder, execution canvas).
Title rule
| Page has… | Title slot | Subtitle slot | Examples |
|---|---|---|---|
| Tabs | Drop. Tabs are the identity. | Drop. UseInfoCard inside tab content if needed. | Logins, Admin → Models / Usage |
| Editorial hero | Drop. Hero replaces it. | Drop. Hero supporting copy carries it. | Overview |
| Start composition | Drop. Split editorial prompt replaces it. | Drop. Primary composer and rail copy carry the context. | Create / get started |
| Custom resource header | Resource name · dynamic content | Drop. | Workflow, Execution, Batch detail pages |
| Plain top-level route, no tabs | Optional. Prefer dropping — sidebar already says it. | Optional. Treat as a smell. | API keys, Integrations, Manage users |
Tabs — pick the API by semantic, not by look
Tabs (Radix)
Internal sub-sections
Self-contained. The user can't open new ones, drag them, or close them. Internal state owns the active tab.
ChromeTabs
Documents / routes
Browser-like. Tabs are openable, draggable, closable. Outer state owns the list.
Both render the same chrome-tab visual — same surface, same shoulder, same inverted corners on the active tab. The split is about what the tabs mean, not how they look.
The Start page shape: a canvas-first decision surface for creating a workflow, onboarding a user, or choosing a first template. One editorial composition may span the primary composer and the supporting template rail, but everything below returns to compact Inter UI.
Canonical ingredients: composer first, primary-tinted interactive controls, neutral/card surfaces, compact learning cards, and a collapsible supporting rail. The rail dimensions are composition constraints, not global spacing tokens.
Describe a repeatable workflow, then let Astro help turn it into a reliable browser or computer-use agent.
Find every Bay Area dental practice accepting new patients, check appointment availability, and summarize the best options.
or start blank
Learn Asteroid
Get the most out of Asteroid
Pro tip
Start broad
Describe the job in plain English first. Astro can help split it into faster scripted steps.
Say what is missing and what to do. Page scale on an empty page or tab;size="sm" inside tables, cards and panels. Filtered-to-nothing offers to clear filters.
Loading chrome that mirrors the final layout. Bars use bg-muted and a 1.5s opacity pulse. Never a single tall blob. Never primary-tinted placeholders. See DESIGN.md §Skeletons.
Primitive. bg-muted, 1.5s opacity pulse
Option card. Same shell as OptionCard
Data table. Card, muted header, compact rows, pagination footer
Collection page. Action row, launcher doors, table
A realistic dashboard combining all components
Monitor and manage your workflow runs
Monday morning · 14 Apr 2026
A quiet weekend — everything held.
Eight workflows ran through2,840 executionswhile you were away. Three flagged themselves for review, one finished early.
Total Runs
Success Rate
Avg Duration
Active workflows
| Execution | Workflow | Status | Duration | Started | Actions |
|---|---|---|---|---|---|
| exec_a1b2c3 | Insurance Scraper | Completed | |||
| exec_d4e5f6 | Form Filler v3 | Running | |||
| exec_g7h8i9 | Data Extractor | Failed | |||
| exec_j0k1l2 | QA Automation | Completed |
Filter · Failed · last 24h
status=failed 24h| Execution | Workflow | Status | Duration | Started | Actions |
|---|---|---|---|---|---|
Nothing failed yesterday. Every one of the 412 runs in the last 24 hours completed clean. You should take the rest of the afternoon off. | |||||
A realistic execution progress feed with collapsible turns, actions, user messages, and chat input
The AI builder assistant conversation with messages, function calls, reasoning blocks, and rich input
Single rule, applied everywhere a moment-in-time is shown. Always relative on screen, single-letter compact, monospace, tabular-nums. Hover for the absolute moment in the user's timezone plus UTC.
See DESIGN.md §Time & Durationfor the convention table and rationale.
Relative time · past
Relative time · future
Absolute time
Tooltip contract — local-with-offset + UTC, mirrored on every time component:
Duration
Prefix prop · one phrase, one font
Pass a `prefix` to render a label inline with the time, in the same mono coordinate font. The whole phrase reads as one unit and the screen-reader label includes the prefix automatically. Prefer this over writing the prefix as plain text next to the component.
Do · Don't
Do
Don't
Single-line labels that can grow arbitrarily long — workflow names, profile names, integration names, file names, schedule labels — get the same treatment everywhere: cap the visible width, ellipsize, and mount a tooltip with the full label only when the visible text is actually clipped. The conditional tooltip is the whole point — a tooltip on an already-visible label is noise.
Use <TruncatedText> from @workspace/ui/components/truncated-text for the standard case. Drop down to useIsTruncated from @workspace/ui/hooks/use-is-truncated only when you need to control the tooltip's open state externally (e.g. suppress it while a sibling popover is open). See DESIGN.md §Tables → Truncated text for the full rules.
Standalone · single width cap
Tooltip mounts only on the third row — the first two fit inside their cap. Hover any row to confirm; tooltips on already-visible labels are noise.
Inside a TableLink with trailing affordance
Pair with min-w-0 flex-1 on the text and shrink-0 on the trailing icon so the affordance always renders, even when the name is clipped.
Inside a table cell — the canonical use
The third row's tooltip carries the full label without forcing the column wider. Resize the page narrower — the second row eventually starts truncating too, and its tooltip mounts the moment the measurement crosses over.