Design Reference · Phase 2

Admin UI
Standards

Canonical patterns for five recurring UI structures in the Sixhands admin prototype. All examples use live design tokens. Follow these specs exactly — no one-offs.

Inline Alert Top Nav Bar PageHead + Actions Table List Detail Panel
01

Inline Alert

Component: InlineAlert · File: shared-blocks.jsx · Props: kind, children, action?

All kinds — live rendering
info — Contextual information visible to real users. POS constraints, export limits, read-only scope explanations.
Learn more →
!
warn — Warnings requiring attention before acting. Billing issues, destructive action confirmations, MFA off.
error — Active error state. Form failures, sync errors, permission blocks. role="alert" set automatically.
success — Persistent confirmation after a completed action. Save confirmed, invite sent, export ready.
i
note (prototype only) — Annotation for DX about scope, constraints, or deferred features. Amber surface so DX identifies these at a glance. Never in production UI.
When to use each kind
KindUse whenDon't use when
info Persistent context the user needs on-page — POS constraint, read-only scope, data lag warning Content is a prototype note (→ note). Content is a confirmation (→ success).
warn Attention needed before proceeding — billing degraded, destructive confirmation, time-sensitive Flagging prototype-only scope limits for DX (→ note).
error An error occurred and action is needed — form failed, sync broken, auth rejected Something might go wrong (→ warn). Toast is better for transient errors.
success Action completed, user needs persistent confirmation on the page Transient confirmations (→ Toast). Neutral entity status (→ StatusPill).
note Prototype only. Scope constraints, deferred features, DX implementation notes Never in production UI. If real users see it, use info or warn.
Specs
PropertyValue
Padding12px 16px
Border radiusvar(--r-sm) = 8px
Icon22×22px circle · filled with fg color · cream text · always present
Body font500 14px/1.5 Inter
Optional actionRight-aligned · same fg color · 600 weight · single CTA only — never two actions in one alert
PlacementBelow PageHead, above FilterBar / content. Never inside a table row or drawer header.
StackingMax 2 per page. If warn + info both present, warn goes first.
✓ Do
  • Use note for any text starting with "DX:" or describing prototype scope
  • Place alert directly below PageHead as first child in page body
  • Use success for persistent confirmations; Toast for transient ones
  • Keep body text to 1–2 lines — link out for more detail
✗ Don't
  • Don't use info or warn for prototype notes — DX can't tell the difference
  • Don't stack more than 2 alerts on a single page
  • Don't place an InlineAlert inside a DetailDrawer header — put it in the drawer body
  • Don't use error for "might fail" — that's warn
02

Top Nav Bar

Shell component · Rendered in: index.html App shell · CSS class: .topbar

Live rendering
GA · Global
G
Country Manager (Philippines)
CM · PH
A
Outlet Manager
OM · BGC
M
Anatomy
ZoneContentsNotes
Left Hamburger menu icon only (Ic.menu(28)) Opens the side nav drawer on click. No logo here — logo lives in the drawer.
Right Notification bell → Scope chip Always in this order, left to right. No other elements.
Scope chip ROLE · SCOPE label + avatar circle Avatar: always --forest background, --lime text, first initial uppercase. Use UserMenuChip component — never inline.
Scope label format GA · Global / CM · PH / AM · {franchisee} / OM · {outlet} Role abbreviation, then scope. Abbreviate long names to ~8 chars.
Specs
PropertyValue
Padding20px 30px 0 30px (top-heavy; page content starts below)
Backgroundvar(--cream)
Icon buttons36×36px · radius var(--r-sm) · color var(--forest) · hover: rgba(0,106,86,.08) fill
Scope chipRadius var(--r-pill) · background var(--mint-soft) · padding 4px 6px 4px 10px
Avatar28×28px circle · background var(--forest) · color var(--lime) · 700 13px Inter
Scope label700 12px Inter · color var(--forest) · letter-spacing .02em
✓ Do
  • Always use --forest for the avatar background (not --orange)
  • Always show ROLE · SCOPE in the chip — never just the role
  • Use UserMenuChip component for the scope chip, not inline JSX
  • Keep right zone to: bell + scope chip only
✗ Don't
  • Don't use --orange for the avatar — that's a known bug, not a variant
  • Don't add extra icon buttons (search, help, etc.) without design approval
  • Don't put the Sixhands logo in the topbar — it lives in the side nav drawer
  • Don't vary the topbar per role — it's identical except for the scope chip text
04

Table List

Component: Table · File: shared.jsx · Above: FilterBar · Below: Pagination, Showing

Live rendering — Franchisees table
Franchisee Country Outlets Status
Thai Hots Group 🇵🇭 Philippines 12 Active
Manila Fresh Co. 🇵🇭 Philippines 4 Pending
Green Bowl PH 🇵🇭 Philippines 7 Active
No results match your filter
Showing 3 of 23 franchisees
Rules
RuleDetail
Primary column Every table has exactly one primary column — the entity name/identifier. Style: 700 14px/1.2 Inter, color var(--forest). Always the leftmost data column. Use render: r => <span style=…> — never global inline styles repeated per-table.
Row click Clicking any row opens the DetailDrawer for that record. The row must have cursor: pointer and a hover background of rgba(0,106,86,.03). Never navigate away on row click — that's what the drawer is for.
Row actions Reveal on hover only (opacity 0 → 1). Max 2 icon buttons per row. Use iconbtn class for the 28×28px icon buttons. Place in rightmost column.
Numeric columns Use Mono component (or font-feature-settings: "tnum") for IDs, counts, amounts. Right-align currency columns only.
Empty state Filtered zero results: inline empty row with muted italic message. No records at all: use EmptyState component below the table header.
FilterBar Always present above a table that can be filtered. Use FilterBar component — never custom filter UI.
Pagination Always pair with Showing (record count) on the left, Pagination buttons on the right. Never one without the other.
Specs
PropertyValue
Header font700 11px/1 Inter · uppercase · letter-spacing .08em · color var(--mint)
Header border1.5px solid var(--line)
Row border1px solid var(--line)
Cell padding13px 16px 13px 0 (no left padding on first col)
Row hover bgrgba(0,106,86,.03)
Primary col style700 14px/1.2 Inter · color var(--forest)
Secondary col style400 13px/1.4 Inter · color var(--ink-2)
Numeric col style600 12px/1 monospace · font-feature-settings "tnum"
✓ Do
  • Always define the primary column style in the columns array's render function — not repeated inline across screens
  • Use FilterBar above every filterable table
  • Open DetailDrawer on row click — never navigate away
  • Show both Showing count and Pagination together
✗ Don't
  • Don't repeat font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" inline in every table's render — extract to a shared pattern
  • Don't show row actions at full opacity — they reveal on hover only
  • Don't add a "View" or "Details" text link in the row — row click IS the view action
  • Don't use a custom sort header — use the Table component's built-in sort
05

Detail Panel

Component: DetailDrawer · File: shared-blocks.jsx · Props: open, onClose, title, subtitle?, tabs?, actions?, children

Live rendering — Detail panel open state
Franchisees
FranchiseeOutletsStatus
Thai Hots Group12Active
Manila Fresh Co.4Pending
Manila Fresh Co.
franch_ph_0042
Status
Pending
Country
🇵🇭 Philippines
Contact
Ana Reyes · ana@manilafresh.ph
Outlets
4 active · 1 pending
Rules
RuleDetail
Width 700px canonical. CSS class .sub-drawer sets width: 700px — this wins over the component's width prop default of 560px. Use the component prop only to override to a narrower width for simple read-only panels.
Trigger Opened by row click in a table. Never opened by a button labeled "View" — the row IS the trigger.
Sections vs. tabs Use Tabs when there are 3+ distinct content groups (e.g., Overview · Outlets · Audit). Use plain sections (with FormSection or KeyValueGrid) when content is a single coherent group.
Header Title: entity name. Subtitle: ID or secondary identifier (e.g., franch_ph_0042). Close button top-right — always present. No status pill in the header — put it in the body.
Action buttons Bottom of drawer, right-aligned. Max 2 actions. Primary rightmost. Never a destructive action without a confirm step (ConfirmModal).
Scrim Always shows behind the open drawer via .sub-scrim.open. Clicking the scrim closes the drawer (onClose). ESC key also closes — handled by the component automatically.
Inline alert in drawer Place InlineAlert in the drawer body, below the header — never in the title or subtitle props.
Specs
PropertyValue
Width700px (CSS canonical) · override via width prop for narrow panels only
Backgroundvar(--cream)
Inner padding30px all sides (.sub-inner)
Gap between sections24px column gap
Title font700 22px/1.2 Nohemi · color var(--forest)
Subtitle font400 13px/1 Inter · color var(--ink-2)
Shadow-8px 0 40px rgba(0,0,0,.22)
Slide animationtransform: translateX(100%) → translateX(0) · 350ms cubic-bezier(.2,.8,.2,1)
Focus trapBuilt into component — Escape closes, Tab cycles within drawer
✓ Do
  • Use the CSS width (700px) as canonical — don't pass a width prop unless narrowing for a simple panel
  • Use Tabs for 3+ content groups; FormSection/KeyValueGrid for single-group content
  • Place action buttons at the bottom right, primary rightmost
  • Show a ConfirmModal before any destructive action
✗ Don't
  • Don't open a drawer from a "View" button — row click is the trigger
  • Don't put a status pill or InlineAlert in the drawer header props — both go in the body
  • Don't place form submit buttons in the PageHead actions when the form is inside the drawer
  • Don't nest drawers — one drawer at a time