A design system for running trips, not reading dashboards
The living reference for the GoDND operator portal. It has two parts: the guidelines and token architecture that govern every decision, and a component library showing each building block and its variants — rendered live from the same code that ships in the product.
Six non-negotiable rules
An operator opens the portal to find out what needs them — a trip awaiting approval, a guest waiting on a reply. Every rule serves that question, and each is enforced in code rather than left to discretion.
Status color is never hardcoded
Every dot, badge, KPI value and notice resolves through statusForExperience() or worstStatus(). The status atoms take a Status, not a color — there is no prop a raw hex could arrive through.
Status colors never carry identity
Green, amber and red say whether something needs the operator — nothing else. Experience types, regions, categories and chart series use brand, info and neutral, so a red line never reads as an alarm by accident.
Sort by what needs attention
Lists lead with what is blocked: changes requested, then under review, then live. Bookings by nearest departure, enquiries by longest unanswered. Alphabetical order buries the one row that matters.
No icon-only control without a label
Every icon button carries a visible label or an aria-label. The collapsed 66px nav expands on hover and on keyboard focus, so its labels are reachable without a mouse.
Never a blank screen, never a lost draft
Lists load their rows on arrival, and an empty list names the next action. The experience builder saves every step as the operator types — closing the tab never costs work.
Every number has context
“213 bookings” alone says little. Pair each value with its comparison: versus last month, seats left of capacity, or the group total a variable price produces.
Atomic design
The interface is composed bottom-up. Tokens feed atoms, atoms compose into molecules and organisms, and templates arrange them into the pages operators actually use.
Tokens
FoundationThree-tier CSS custom properties mirrored by the Tailwind theme — the vocabulary every atom is built from.--brand · --card-border · --critical-tint · --brand-solid · radius · type scaleAtoms
IndivisibleThe smallest units — one element, one job.StatusDot · Badge · Button · TextInput · Select · Checkbox · TokenChip · Avatar · SparklineMolecules
Small groupsA handful of atoms bound into a reusable unit.StatusBadge · KpiCard · PillTabs · Field · AffixInput · ChipSelect · Notice · SearchInputOrganisms
SectionsStandalone, composed regions of the interface.ExperiencesBoard · Panel · Sidebar · PageBar · RecordDrawer · WizardRail · DropdownMenu · ToastTemplates
LayoutPage skeletons — arrangement without real data.AppShell (nav + one floating surface card) · list + record drawer · rail + step formPages
InstancesTemplates filled with an operator's real data.Experiences · Experience detail · New experience (7 steps) · Design system
Three-tier token architecture
No component ever names a raw value. Each tier references the one beneath it, so intent stays legible and a single edit ripples predictably through the whole platform.
--emerald-700: #047857--brand: var(--emerald-700)--brand-solid: var(--emerald-700)Primitive tokens
The raw palette, named by hue and lightness, never by purpose.
Neutral ramp · slate
A blue-tinted grayscale. Every surface, border and text color is drawn from here.--white --slate-25 --slate-50 --slate-75 --slate-100 --slate-125 --slate-150 --slate-200 --slate-500 --slate-550 --slate-600 --slate-900 Emerald · brand
GoDND green. 600 is the base for dots and rails; 700 is the AA-safe step for brand text and for fills that carry a white label; 800 is the hover / pressed step.--emerald-50 --emerald-600 --emerald-700 --emerald-800 Blue · info
Informational only — notices, the info status, links inside prose. Kept separate from brand so an info toast never reads as a brand action.--blue-50 --blue-600 --blue-700 --blue-800 Violet · accent
Onboarding and walkthrough moments only — never status, never a category.--violet-50 --violet-600 Status hues
Reserved for status. Each hue has a base (600) for dots and borders, and a deeper step (700) for status text and for fills that carry white text.--green-600 --green-700 --amber-600 --amber-700 --red-600 --red-700 Semantic tokens
What the platform actually references. Each maps to exactly one primitive.
| Token | Primitive | Value | Usage |
|---|---|---|---|
--canvas | --slate-25 | App gutter / page background — a shade darker than the card | |
--card | --white | The single floating surface card | |
--panel | --slate-50 | Inset panels, inputs, the pill-tab track | |
--panel-2 | --slate-100 | Row hover, pressed panel | |
--raised | --white | Menus, popovers, toast | |
--border-subtle | --slate-75 | Hairline dividers inside a panel | |
--border-panel | --slate-150 | Panel, control and drawer borders | |
--border-strong | --slate-200 | Hover / emphasized borders | |
--text-primary | --slate-900 | Headings, values, primary copy | |
--text-secondary | --slate-600 | Labels, secondary copy | |
--text-muted | --slate-550 | Captions, hints, meta — clears AA on every surface | |
--brand | --emerald-700 | Focus, active rail, brand text and icons | |
--brand-muted | --emerald-50 | Active nav / tab / row background | |
--accent | --violet-600 | Onboarding & walkthrough accent | |
--accent-muted | --violet-50 | Accent surface | |
--healthy | --green-600 | Live and bookable — nothing to do | |
--warning | --amber-600 | Waiting on someone — under review, payment pending | |
--critical | --red-600 | Blocked until the operator acts — changes requested | |
--info | --blue-600 | Informational notices | |
--neutral | --slate-500 | Draft, disabled, archived — not trading, nothing wrong |
Component tokens
Interaction states and composite decisions. The -solid steps exist for one reason: a fill that carries white text has to clear 4.5:1, which the base status hues do not.
| Token | Resolves to | Value | Usage |
|---|---|---|---|
--brand-solid | --emerald-700 | Primary button, avatar, selected calendar day | |
--brand-hover | --emerald-800 | Primary fill hover | |
--brand-active | --emerald-800 | Primary fill pressed | |
--brand-on-muted | --emerald-700 | Text / icon on a --brand-muted surface | |
--on-brand | --white | Text on any solid fill | |
--healthy-solid | --green-700 | Status badge fill · healthy | |
--warning-solid | --amber-700 | Status badge fill · warning | |
--critical-solid | --red-700 | Status badge fill · critical | |
--info-solid | --blue-600 | Status badge fill · info | |
--neutral-solid | --slate-500 | Status badge fill · neutral | |
--card-border | --slate-125 | Hairline on every card, panel and table surface | |
--card-shadow | slate-900 · 5% + 10% | Floating-card elevation | |
--critical-fg | --red-700 | Status as text — on a critical tint or a plain surface | |
--warning-fg | --amber-700 | Status as text — on a warning tint or a plain surface | |
--healthy-fg | --green-700 | Status as text — on a healthy tint or a plain surface | |
--critical-tint | red-600 · 10% | Badge / notice critical fill | |
--warning-tint | amber-600 · 12% | Badge / notice warning fill | |
--healthy-tint | green-600 · 12% | Badge / notice healthy fill | |
--info-tint | blue-600 · 10% | Info surface fill | |
--focus-ring | 0 0 0 3px · emerald-700 · 18% | Text input / select focus glow |
Color
The card is white and the canvas sits a shade darker, so the single surface card reads as floating. Everything else inside it is a quieter step of the same slate.
--canvas Gutter--card Surface card--panel Panel--panel-2 Hover--raised Raised / overlay--brandBrand--brand-mutedBrand muted--accentAccent--healthyHealthy--warningWarning--criticalCritical--infoInfo-fg step for status written as text; and a -solid step for fills that carry a white label. Picking the step by job is what keeps every pair at AA.Contrast
Computed live from the tokens · WCAG AA is 4.5:1| Pair | Tokens | Ratio |
|---|---|---|
| Primary text on card | --text-primary on --card | — |
| Secondary text on card | --text-secondary on --card | — |
| Muted text on card | --text-muted on --card | — |
| Muted text on panel | --text-muted on --panel | — |
| Muted text on hover row | --text-muted on --panel-2 | — |
| Brand text on card | --brand on --card | — |
| Critical text on card | --critical-fg on --card | — |
| Warning text on card | --warning-fg on --card | — |
| Brand-on-muted on muted | --brand-on-muted on --brand-muted | — |
| Primary button label | --on-brand on --brand-solid | — |
| Primary button, hover | --on-brand on --brand-hover | — |
| Healthy badge label | --on-brand on --healthy-solid | — |
| Warning badge label | --on-brand on --warning-solid | — |
| Critical badge label | --on-brand on --critical-solid | — |
| Neutral badge label | --on-brand on --neutral-solid | — |
Typography
One typeface: Roboto, for everything. Numerals take tabular figures globally, so prices, dates and counts line up in columns without a second, monospaced face.
Spacing & radius
A 14px gutter frames the app; radii step 6 → 9 → 10 → 12px from controls up to the surface card.
--radius-sm6pxChips, inputs, buttons--radius-md9pxPill tabs, menus, notices--radius-lg10pxPanels, cards, tables--nav-w178pxSidebar (always expanded)--gutter14pxCanvas paddingElevation
Depth is carried by soft, cool-gray shadows. The higher the layer, the larger and softer the cast.
--card-shadow--shadow-dropdown--shadow-pop--shadow-toast--shadow-drawerStatus & badges
Status indicators are the heart of the system. Every one takes a Status resolved in lib/status.ts — none of them accepts a color.
Status dot
StatusDot · sizes sm / md / lgTable status dot
.status-dot — critical and warning carry a haloStatus badge
StatusBadge · solid -solid fill, white labelPill badges
Badge · .kpi-chip · .flag — tint + on-tint foregroundExperience state → status
statusForExperience() · the one resolver lists and drawers use| State | Status | Badge |
|---|---|---|
rejected | critical | Changes requested |
under_review | warning | Under review |
active | healthy | Active |
draft | neutral | Draft |
disabled | neutral | Disabled |
archived | neutral | Archived |
Metric cards
KpiCard is the workhorse — label, status chip, value, sparkline and a comparison caption, all colored by the resolved status. Summary pills give the count at a glance.
KPI card
KpiCard · comparison is a required prop (rule 06)Summary pills
.summary-strip · total / critical / warning / healthySparkline
Sparkline · inline SVG, area + line, color from a tokenTables & panels
Panel is the universal card with a header row. Tables right-align every number, lead each row with its status, and sort by what needs attention (rule 03).
Panel + data table
Panel › .data-table · row header · right-aligned numericsExperiences
needs attention first| Experience | State | Bookings | Seats left | Base price |
|---|---|---|---|---|
| Rafting, Camping & Cycling in Upper Assam | Changes requested | 0 | 8 of 8 | ₹98,000 |
| Raw Experience in Meghalaya | Under review | 0 | 4 of 4 | ₹18,500 |
| 7 Day Immersive Experience in Meghalaya | Active | 213 | 3 of 10 | ₹67,000 |
Pill tabs
PillTabs · link tabs for server state, button tabs for view stateCompact tabs
.tabbar · .tab.active — dense contexts, such as inside a drawerFeedback & empty states
When something needs the operator, the notice says what and why as a first-class element instead of hiding it in a tooltip. Empty states always offer the next action.
Notice
Notice · left-border status accent.env.local to see your own experiences.Empty state
EmptyState · icon + title + description + actionNo drafts
Experiences you start but don't send for approval are kept here, saved as you type.
Toast
.toast-bar · transient, auto-dismisses after 5sSkeleton
.skeleton · the loading shape of what is comingForms
Every control is native and shares one focus treatment — a --brand border and the --focus-ring glow. Controls sit quiet on --panel until touched; labels, hints and errors are wired to the control for assistive tech.
Text fields
Field › TextInput · hint · errorShown on the marketplace and on your own site.
Enter an email address, like you@company.com
Select, affix & text area
Select · AffixInput · TextAreaChoices
Checkbox · RadioGroup · .switch · ChipSelect- Meghalaya
Search
.search-wrap · .search-inputOverlays
Menus, the record drawer and tooltips sit on --raised or --card with a soft shadow. Each is built on a Radix primitive in shadcn's composition, so keyboard, focus and Escape behave correctly by default.