Property PrismDev Hub

Prism Design System Specification

Updated Apr 3, 2026

Version: 1.0 Platform: Commercial Real Estate Intelligence Stack: React 19, Mantine 7, MapLibre GL, ECharts, TanStack Router/Query/Table


Migration Notes

This specification targets the following changes from the current codebase:

PropertyCurrentTarget
FontIBM Plex SansInter
Structural accentIndigo #4f46e5Navy #1e3a8a
Action accentIndigo #4f46e5Teal #0d9488
Background#f9fafb#f8fafc
Token namingAd-hoc CSS varsStructured semantic tokens

A. Design Token Table

A.1 Color Tokens — Neutrals

TokenHexUsage
color.background.default#f8fafcPage canvas
color.background.subtle#f1f5f9Zebra rows, filter bars, inset areas
color.background.muted#e2e8f0Disabled fills, skeleton base
color.surface.card#ffffffCards, panels, modals, drawers
color.surface.elevated#ffffffDropdowns, popovers (same fill, separated by shadow)
color.border.default#e2e8f0Card borders, dividers, table lines
color.border.strong#cbd5e1Input borders, active card borders
color.border.focus#0d9488Focus ring color
color.text.primary#111827Headings, body, table content
color.text.secondary#64748bCaptions, helper text, breadcrumbs
color.text.muted#94a3b8Placeholders, disabled text
color.text.inverse#ffffffText on filled buttons/badges

A.2 Color Tokens — Structural & Action

TokenHexRole
color.navy.base#1e3a8aSidebar bg, nav active, structural identity
color.navy.dark#172554Sidebar hover, pressed
color.navy.light#dbeafeNav active background tint (if light sidebar variant)
color.navy.text#eff6ffSidebar text on navy
color.teal.base#0d9488Primary CTA, links, active toggles
color.teal.hover#0f766eButton hover, link hover
color.teal.pressed#115e59Button active/pressed
color.teal.light#ccfbf1Teal badge fill, highlight rows
color.teal.subtle#f0fdfaLight teal wash for selected states

A.3 Color Tokens — Semantic

TokenHexUsage
color.success.base#059669Positive deltas, success badges
color.success.light#d1fae5Success badge fill
color.success.text#065f46Success text on light fill
color.warning.base#d97706Warning badges, attention states
color.warning.light#fef3c7Warning badge fill
color.warning.text#92400eWarning text on light fill
color.error.base#dc2626Validation errors, destructive actions
color.error.light#fee2e2Error badge fill, error row highlight
color.error.text#991b1bError text on light fill
color.info.base#2563ebInformational badges, links
color.info.light#dbeafeInfo badge fill
color.info.text#1e40afInfo text on light fill

A.4 Color Tokens — Interaction States

TokenValueUsage
color.hover.row#f1f5f9Table row hover, list item hover
color.hover.controlrgba(14, 148, 136, 0.08)Button ghost hover (teal tint)
color.active.controlrgba(14, 148, 136, 0.14)Button ghost pressed
color.selected.row#f0fdfaSelected table row
color.selected.strong#ccfbf1Active filter chip, strong selection
color.disabled.bg#f1f5f9Disabled input/button fill
color.disabled.text#94a3b8Disabled label text
color.disabled.border#e2e8f0Disabled input border
color.focus.ring0 0 0 2px #ffffff, 0 0 0 4px #0d9488Focus ring (box-shadow)

A.5 Color Tokens — Chart Palette

TokenHexUsage
color.chart.series.1#0d9488Primary series (teal)
color.chart.series.2#1e3a8aComparison series (navy)
color.chart.series.3#7c3aedTertiary (violet)
color.chart.series.4#ea580cQuaternary (orange)
color.chart.series.5#0284c7Quinary (sky)
color.chart.series.6#c026d3Senary (fuchsia)
color.chart.area.fillrgba(13, 148, 136, 0.10)Area fill under lines
color.chart.grid#e2e8f0Gridlines
color.chart.axis#94a3b8Axis labels, tick marks
color.chart.tooltip.bg#ffffffTooltip background
color.chart.tooltip.border#e2e8f0Tooltip border

A.6 Color Tokens — Map Layer Palette

TokenHexUsage
color.map.building#0d9488Building markers/fills
color.map.comp#2563ebComp markers/fills
color.map.tim#7c3aedTIM markers/fills
color.map.cluster.bg#1e3a8aCluster circle fill
color.map.cluster.text#ffffffCluster count label
color.map.highlight#f59e0bHovered/selected feature
color.map.heat.low#ccfbf1Heatmap low value
color.map.heat.mid#0d9488Heatmap mid
color.map.heat.high#115e59Heatmap high

B. Typography System

B.1 Font Stack

font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;

Enable OpenType features for tabular numerals:

.tabular-nums {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: 'tnum' 1, 'lnum' 1;
}

Apply .tabular-nums to: KPI values, table numeric cells, chart axis labels, stat bars, pagination counts.

B.2 Type Scale

TokenSizeWeightLine HeightLetter SpacingUsage
font.size.pageTitle20px6001.3 (26px)-0.01emPage headings
font.size.sectionTitle16px6001.35 (21.6px)0Card headings, section labels
font.size.subsection14px6001.4 (19.6px)0Form section titles, sub-headings
font.size.body14px4001.5 (21px)0Paragraph text, form labels, table cells
font.size.bodyStrong14px5001.5 (21px)0Emphasized body text, table header cells
font.size.caption12px4001.4 (16.8px)0.01emHelper text, timestamps, badges, stat labels
font.size.captionStrong12px5001.4 (16.8px)0.01emTable column headers (uppercase)
font.size.overline11px6001.4 (15.4px)0.05emSection overlines (uppercase, rare)
font.size.kpi28px7001.2 (33.6px)-0.02emDashboard KPI values
font.size.kpiSmall22px6001.25 (27.5px)-0.01emSecondary stat values

B.3 Hierarchy Rules

  1. Size and weight establish hierarchy before color.
  2. Do not use color alone to distinguish heading levels.
  3. Maximum three weight levels on any single page: 400, 500/600, 700.
  4. Uppercase is reserved for: table column headers (captionStrong) and overlines.
  5. No italic usage in the interface except user-generated content.

C. Spacing, Radius, Density

C.1 Spacing Tokens

TokenValueUsage
spacing.22pxIcon internal padding only
spacing.44pxMicro: between badge icon and label, between inline elements
spacing.66pxInput internal vertical padding (compact)
spacing.88pxDefault gap between sibling fields, card internal padding minimum
spacing.1212pxStack gap between form rows, between stat items
spacing.1616pxCard padding, section gap (comfortable)
spacing.2020pxPage padding (main content)
spacing.2424pxBetween major page sections
spacing.3232pxBetween top-level layout regions

C.2 Radius Tokens

TokenValueUsage
radius.xs4pxBadges, small chips, inline tags
radius.sm6pxButtons, inputs, selects, toggles
radius.md8pxCards, panels, modals, drawers
radius.lg10pxLarge containers (rare)

C.3 Border Specification

PropertyValue
Default border1px solid color.border.default (#e2e8f0)
Strong border1px solid color.border.strong (#cbd5e1)
Focus ringbox-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #0d9488
Divider1px solid color.border.default (horizontal rule within cards)

Borders are the primary visual separation mechanism. Shadows are not used for cards or sections.

C.4 Shadow Tokens

TokenValueUsage
shadow.dropdown0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06)Dropdowns, popovers, combobox lists
shadow.modal0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)Modals, drawers
shadow.tooltip0 2px 8px rgba(0,0,0,0.10)Tooltips

No other shadows. Cards, panels, and sections use borders only.

C.5 Density Modes

Two modes: Comfortable (default) and Compact. Toggle available on Explore and Operations pages.

ElementComfortableCompact
Table row height44px32px
Table cell vertical padding10px4px
Table font size14px13px
Input height36px30px
Input font size14px13px
Button height (sm)30px26px
Button height (md)36px30px
Card padding16px12px
Filter bar padding12px8px
Stack gap (form fields)12px (xs)8px
Badge height22px18px
Badge font size12px11px

Implementation Strategy

Density is managed via a CSS class on the content area and a React context:

// DensityContext.tsx
type Density = "comfortable" | "compact";
const DensityContext = createContext<{
  density: Density;
  setDensity: (d: Density) => void;
}>({ density: "comfortable", setDensity: () => {} });

CSS applies via [data-density="compact"] attribute on the page wrapper:

[data-density="compact"] .prism-table tbody tr { height: 32px; }
[data-density="compact"] .prism-table tbody td { padding: 4px 12px; font-size: 13px; }
[data-density="compact"] .mantine-TextInput-input { height: 30px; font-size: 13px; }
[data-density="compact"] .mantine-Button-root[data-size="sm"] { height: 26px; }
[data-density="compact"] .mantine-Button-root[data-size="md"] { height: 30px; }
[data-density="compact"] .mantine-Card-root { padding: 12px; }

Persist density preference per-page to localStorage under prism:density:{pageKey}.


D. Layout Grid System

D.1 Container Specification

PropertyValue
Max content width1440px (centered when viewport exceeds)
Page padding20px all sides
Page padding (mobile <980px)16px all sides

D.2 Grid System

12-column grid using Mantine SimpleGrid and Grid components.

PropertyValue
Columns12
Gutter16px (comfortable) / 12px (compact)
Column minimumFluid, no fixed minimum

Common Layouts

PatternColumnsBreakpoints
KPI row4 equal{ base: 2, md: 4 }
Metric cards3 equal{ base: 1, sm: 2, lg: 3 }
Explore split8 + 4 (table + sidebar){ base: 12, lg: 8 } + { base: 12, lg: 4 }
Map + panel12 (full width, panel overlays)N/A
Form two-column6 + 6{ base: 1, sm: 2 } via SimpleGrid
Dashboard chart6 + 6{ base: 12, md: 6 }

D.3 Breakpoints

NameValueNotes
xs576pxPhone portrait
sm768pxTablet portrait
md992pxSidebar collapses to drawer
lg1200pxStandard desktop
xl1440pxWide desktop

D.4 Collapse Behavior

BreakpointSidebarGridPage Padding
>= 992pxVisible (240px or 72px collapsed)Full multi-column20px
< 992pxHidden (drawer)Single column stacks16px
< 576pxHidden (drawer)Single column, reduced gaps12px

D.5 Alignment Rules

  • Charts and tables always align to the same left edge within a card or section.
  • Map components are always full-width within their container (no internal margin).
  • Form fields in two-column layout use equal-width columns; labels are above inputs, never side-by-side.
  • KPI cards in a row all have equal height (Mantine Grid with grow).
  • Tables span full card width with no internal horizontal padding beyond cell padding.

E. Z-Index & Layering Model

E.1 Z-Index Scale

Layerz-indexElements
Base content0Page content, cards, tables
Map canvas0MapLibre canvas (rendered in DOM order)
Map overlays100Map legends, layer toggles, floating controls
Map markers100Custom marker overlays
Map popups200MapLibre popups
Sticky header300Table sticky thead, filter bar sticky
Sidebar400Navigation sidebar
Topbar500App header bar
Dropdown600Select menus, combobox lists, popovers
Drawer700Side drawers (create/edit panels)
Modal backdrop800Modal overlay
Modal801Modal content
Tooltip900Tooltips
Command palette950Command launcher overlay
Notification1000Toast notifications

E.2 MapLibre Layering Rules

MapLibre renders its own canvas at z-index 0. UI elements that overlay the map must:

  1. Use position: absolute or position: fixed relative to the map container.
  2. Set pointer-events: none on overlay containers; pointer-events: auto on interactive children.
  3. Never use z-index values that conflict with Mantine's internal popover stack (Mantine popovers use z-index 300 by default — override via theme to 600).

Map controls (zoom, layer toggles, legend) sit at z-index 100 within the map container's stacking context. Since the map container creates its own stacking context, these values don't conflict with page-level z-indices.

E.3 Mantine Z-Index Overrides

// In Mantine theme
{
  other: {
    zIndex: {
      dropdown: 600,
      drawer: 700,
      modal: 801,
      tooltip: 900,
      notification: 1000,
    }
  }
}

Override via component defaultProps:

Popover: { defaultProps: { zIndex: 600 } },
Select: { defaultProps: { comboboxProps: { zIndex: 600 } } },
Modal: { defaultProps: { zIndex: 801 } },
Drawer: { defaultProps: { zIndex: 700 } },
Tooltip: { defaultProps: { zIndex: 900 } },
Notifications: { defaultProps: { zIndex: 1000 } },

F. Shell Architecture

F.1 Sidebar

PropertyValue
Expanded width240px
Collapsed width72px
Background colorcolor.navy.base (#1e3a8a)
Text colorcolor.navy.text (#eff6ff)
Item height36px
Item horizontal padding12px
Item border radiusradius.sm (6px)
Icon size20px
Icon color (default)rgba(255,255,255,0.65)
Icon color (active)#ffffff
Active item backgroundrgba(255,255,255,0.12)
Active item text color#ffffff
Active item left accent3px solid color.teal.base
Hover item backgroundrgba(255,255,255,0.08)
Section labelfont.size.overline (11px), uppercase, rgba(255,255,255,0.45)
Section gapspacing.24 (24px) between groups
Divider1px solid rgba(255,255,255,0.10)
Collapse transitionwidth 200ms ease
Admin sectionCollapsible, with chevron indicator
Collapse toggle buttonBottom of sidebar, 20px icon, border-top separator

F.2 Topbar

PropertyValue
Height52px
Backgroundcolor.surface.card (#ffffff)
BorderBottom: 1px solid color.border.default
Padding horizontal20px
Left sectionBrand mark (logo, not wordmark) — 28px height
Center-left sectionBreadcrumb trail + page title
Center sectionGlobal search trigger (min 280px, max 400px)
Right sectionNotification bell + Clerk UserButton
  • Font: font.size.caption (12px), color color.text.secondary
  • Separator: / character, color color.text.muted
  • Current page: font.size.body (14px), weight 600, color color.text.primary
  • Breadcrumb and title stack vertically in a single line-height-adjusted container

Global Search Trigger

PropertyValue
Height32px
Backgroundcolor.background.default (#f8fafc)
Border1px solid color.border.default
Border radiusradius.sm (6px)
Placeholder text"Search buildings, comps, TIMs..."
Keyboard hintRight-aligned ⌘K badge
On click/keyboardOpens Command Launcher overlay

F.3 Command Launcher

PropertyValue
Activation⌘K / Ctrl+K
Width560px centered
Top offset20% from top of viewport
Backgroundcolor.surface.card
Border1px solid color.border.default
Shadowshadow.modal
Border radiusradius.md (8px)
Input height44px, font font.size.body
Result item height40px
Result group headerfont.size.caption, uppercase, color.text.muted
Active resultcolor.background.subtle background
Debounce200ms for data search mode
Max visible results8 (scrollable)

G. Interaction Pattern Standards

G.1 Hover States

ElementHover Effect
Table rowBackground color.hover.row (#f1f5f9)
Button (filled)Darken fill by one step (e.g., teal.hover)
Button (outline)Background color.hover.control
Button (subtle/ghost)Background color.hover.control
Sidebar itemBackground rgba(255,255,255,0.08)
Card (clickable)Border color color.border.strong, no shadow, no transform
LinkUnderline, color color.teal.hover

No hover lift/translate effects. No scale transforms. No shadow additions on hover.

G.2 Focus States

All interactive elements use a consistent focus ring:

:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #0d9488;
}
  • 2px white inner ring for contrast on any background.
  • 2px teal outer ring for visibility.
  • Applied via :focus-visible only (not :focus) to avoid showing on mouse click.
  • Override Mantine's default focus styles in theme.

G.3 Keyboard Navigation

ContextKeysBehavior
Global⌘K / Ctrl+KOpen command launcher
GlobalN (when not in input)Open "New" menu
GlobalEscapeClose topmost overlay
Table↑ ↓Navigate rows (when table has focus)
Command launcher↑ ↓ EnterNavigate and select results
Form⌘Enter / Ctrl+EnterSubmit form
FormEscapeCancel/close form
SidebarTabNavigate between items

G.4 Form Validation

StateVisual Treatment
DefaultBorder color.border.strong, no helper text
ErrorBorder color.error.base (#dc2626), error text below in color.error.base, font.size.caption
Helper textBelow input, color.text.secondary, font.size.caption
Required indicatorRed asterisk after label
DisabledBackground color.disabled.bg, border color.disabled.border, text color.disabled.text

Validation is shown on blur or on submit, never on keystroke.

G.5 Destructive Action Pattern

  1. Destructive buttons use color.error.base as fill.
  2. Single-step destructive actions (delete a row): Confirmation popover with explicit action name. E.g., "Delete building 'Park Tower'? This cannot be undone." with "Cancel" (outline) and "Delete" (filled red) buttons.
  3. Bulk destructive actions: Modal with count and explicit confirmation. E.g., "Delete 12 buildings? This cannot be undone."
  4. Never use browser confirm() dialogs.

G.6 Disabled State

  • Opacity 0.5 is not used. Instead:
    • Background: color.disabled.bg
    • Text: color.disabled.text
    • Border: color.disabled.border
    • Cursor: not-allowed
    • No hover/focus effects

G.7 Motion

AnimationDurationEasingUsage
Drawer slide200msease-outDrawer open/close
Collapse expand200mseaseAccordion, collapsible sections
Fade in150mseaseModal/drawer backdrop, tooltips
Sidebar collapse200mseaseSidebar width transition
NoneNo card hover animations, no page transitions, no skeleton shimmer

No spring, bounce, or physics-based easing. No transition-delay. No animation keyframes except loading spinners.


H. Page Pattern Standards

H.1 Dashboard

┌──────────────────────────────────────────────────────┐
│ PageHeader: "Dashboard"                              │
├──────────────────────────────────────────────────────┤
│ Filter Bar (collapsible)                             │
│ [Property Type ▾] [Class ▾] [Status ▾] [Market ▾]   │
├──────────────────────────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐                 │
│ │ KPI  │ │ KPI  │ │ KPI  │ │ KPI  │  SimpleGrid 4   │
│ └──────┘ └──────┘ └──────┘ └──────┘                 │
├──────────────────────────────────────────────────────┤
│ ┌────────────────────┐ ┌────────────────────┐        │
│ │ Chart Card         │ │ Map Card           │  2-col │
│ │                    │ │                    │        │
│ └────────────────────┘ └────────────────────┘        │
├──────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌────────────┐ ┌────────────┐        │
│ │ Activity   │ │ Activity   │ │ Activity   │  3-col  │
│ └────────────┘ └────────────┘ └────────────┘        │
└──────────────────────────────────────────────────────┘
  • Filter bar sits above KPIs, collapsible via Collapse component.
  • KPI cards: font.size.kpi for value, font.size.caption for label, tabular-nums.
  • KPI delta indicator: green up arrow or red down arrow, font.size.caption.
  • Map card: Fixed 340px height within card.
  • Activity cards: time-relative labels ("2h ago"), entity link.

H.2 Explore Pages (Buildings, Comps, TIMs)

┌──────────────────────────────────────────────────────┐
│ PageHeader: "Buildings"  [+ New Building]             │
├──────────────────────────────────────────────────────┤
│ DataTableToolbar                                      │
│ [🔍 Search...] [Density ▾] [View: Table | Map]       │
├──────────────────────────────────────────────────────┤
│ StatBar: Total: 1,234 | Filtered: 567 | Page 1 of 57│
├──────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ Table (full width, sticky header)                │ │
│ │ ┌────┬────────┬──────┬───────┬────────┬────────┐ │ │
│ │ │    │ Name   │ Type │ RBA   │ Market │ Status │ │ │
│ │ ├────┼────────┼──────┼───────┼────────┼────────┤ │ │
│ │ │    │        │      │       │        │        │ │ │
│ │ └────┴────────┴──────┴───────┴────────┴────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│ Pagination: ← Page 1 of 57 →                        │
└──────────────────────────────────────────────────────┘
  • Table view is default. Map view swaps table for a 580px map.
  • Create/edit opens a right-side Drawer (480px width).
  • Drawer has its own header (title + close), scrollable body, sticky footer (Cancel + Save).
  • Search and filters persist to URL via TanStack Router search params.
  • Density toggle available in toolbar.

H.3 Map Explore Page (Immersive)

┌──────────────────────────────────────────────────────┐
│ Topbar (52px)                                        │
├──────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ MAP (fills remaining viewport height)            │ │
│ │                                                  │ │
│ │  ┌─────────────────┐         ┌──────────────┐   │ │
│ │  │ Filter Panel    │         │ Layer Toggle  │   │ │
│ │  │ (floating left) │         │ (floating rt) │   │ │
│ │  └─────────────────┘         └──────────────┘   │ │
│ │                                                  │ │
│ │  ┌──────────────────────┐                        │ │
│ │  │ Legend (bottom left)  │                        │ │
│ │  └──────────────────────┘                        │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
  • Map is NOT inside a card. It IS the page.
  • Map height: calc(100vh - 52px) (viewport minus topbar).
  • Sidebar can be collapsed on this page for maximum map area.
  • Floating panels have background: rgba(255,255,255,0.92), backdrop-filter: blur(8px), border, radius.md, shadow.dropdown.
  • Feature click opens a right-side drawer or bottom panel with entity details.

H.4 Analytics Pages

┌──────────────────────────────────────────────────────┐
│ PageHeader: "Vacancy Analytics"                       │
├──────────────────────────────────────────────────────┤
│ Controls Bar                                          │
│ [Quarter ▾] [Dimension ▾] [Metric ▾] [Export ▾]      │
├──────────────────────────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐                 │
│ │ KPI  │ │ KPI  │ │ KPI  │ │ KPI  │                  │
│ └──────┘ └──────┘ └──────┘ └──────┘                 │
├──────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────┐ │
│ │ Primary Chart (trend line, 400px height)         │ │
│ └──────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│ ┌────────────────────┐ ┌────────────────────┐        │
│ │ Comparison Chart   │ │ Detail Table       │  2-col │
│ └────────────────────┘ └────────────────────┘        │
└──────────────────────────────────────────────────────┘
  • KPIs use tabular-nums and font.size.kpi.
  • Charts are inside cards with padding: 16px and a card title.
  • Chart canvas minimum height: 300px.
  • SegmentedControl for dimension switching, not tabs.

H.5 Operations Pages (Queue, Saved Views)

┌──────────────────────────────────────────────────────┐
│ PageHeader: "Needs Review"  [Bulk Actions ▾]          │
├──────────────────────────────────────────────────────┤
│ StatBar: Pending: 45 | In Progress: 12 | Done: 234  │
├──────────────────────────────────────────────────────┤
│ DataTableToolbar                                      │
│ [🔍 Search...] [Status ▾] [Assigned ▾] [Density ▾]   │
├──────────────────────────────────────────────────────┤
│ Table with priority indicators (left border accent)   │
│ Row actions: [View] [Assign] [Complete] [Dismiss]     │
├──────────────────────────────────────────────────────┤
│ Pagination                                            │
└──────────────────────────────────────────────────────┘
  • Priority rows: 3px left border in color.error.base (high), color.warning.base (medium).
  • Row actions via RowActionsMenu (three-dot menu).
  • Density toggle available.

H.6 Data Management Pages

┌──────────────────────────────────────────────────────┐
│ PageHeader: "Owners"  [+ New Owner]                   │
├──────────────────────────────────────────────────────┤
│ DataTableToolbar                                      │
│ [🔍 Search...]                                        │
├──────────────────────────────────────────────────────┤
│ Table (standard, no density toggle)                   │
├──────────────────────────────────────────────────────┤
│ Pagination                                            │
└──────────────────────────────────────────────────────┘
  • Simpler than Explore pages — no map view, no density toggle.
  • Create/edit via right-side Drawer.
  • Row actions: Edit, Delete (with confirmation).

H.7 Admin Pages

┌──────────────────────────────────────────────────────┐
│ PageHeader: "System Setup"                            │
├──────────────────────────────────────────────────────┤
│ Tab navigation: [General] [Database] [Integrations]   │
├──────────────────────────────────────────────────────┤
│ Card-based settings groups                            │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Section Title                                    │ │
│ │ Key-value settings with inline edit              │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
  • Tabs for sub-sections (Mantine Tabs component).
  • Settings displayed as labeled value pairs with Edit buttons.
  • Admin-only access badge in page header.
  • Destructive settings use color.error.base buttons with confirmation modal.

H.8 Drawer vs Modal Decision Matrix

ActionComponentWidthNotes
Create entityDrawer (right)480pxScrollable body, sticky footer
Edit entityDrawer (right)480pxPre-filled form
View entity detailDrawer (right)560pxRead-only sections
Delete confirmationModal440pxCentered, short content
Bulk action confirmationModal480pxCount + confirmation
Global settingsModal560pxCentered
Import wizardModal640pxMulti-step
Export settingsModal480pxOptions + action

I. Map Standards (MapLibre)

I.1 Basemap Philosophy

  • Light, desaturated basemap. Streets and labels visible but not prominent.
  • Use a light-themed vector tile style (e.g., MapTiler Positron, Carto Positron, or custom OSM style).
  • Raster tile fallback: Current OSM tiles are acceptable as interim; migrate to vector tiles for style control.
  • Basemap text: #64748b for labels, #e2e8f0 for roads, #f1f5f9 for land fill.
  • The basemap must recede behind data layers.

I.2 Marker Styling Hierarchy

EntityShapeSizeFillBorder
BuildingCircle10px radiuscolor.map.building (#0d9488)2px #ffffff
CompDiamond (rotated square)10pxcolor.map.comp (#2563eb)2px #ffffff
TIMTriangle10pxcolor.map.tim (#7c3aed)2px #ffffff
SelectedSame shape12pxcolor.map.highlight (#f59e0b)2px #ffffff
HoveredSame shape11pxOriginal + 20% lighter2px #ffffff

MapLibre Style JSON (Circle Layer Example — Buildings)

{
  "id": "buildings-circle",
  "type": "circle",
  "source": "buildings",
  "paint": {
    "circle-radius": [
      "interpolate", ["linear"], ["zoom"],
      8, 3,
      12, 6,
      16, 10
    ],
    "circle-color": "#0d9488",
    "circle-stroke-color": "#ffffff",
    "circle-stroke-width": 2,
    "circle-opacity": 0.85
  }
}

I.3 Cluster Styling

PropertyValue
Cluster fillcolor.map.cluster.bg (#1e3a8a)
Cluster text#ffffff, font.size.caption (12px), weight 600
Cluster border2px solid #ffffff
Cluster sizesSmall (< 10): 24px, Medium (10-99): 32px, Large (100+): 40px
Cluster opacity0.9

MapLibre Cluster Style JSON

{
  "id": "clusters",
  "type": "circle",
  "source": "buildings",
  "filter": ["has", "point_count"],
  "paint": {
    "circle-color": "#1e3a8a",
    "circle-radius": [
      "step", ["get", "point_count"],
      12, 10,
      16, 100,
      20
    ],
    "circle-stroke-width": 2,
    "circle-stroke-color": "#ffffff",
    "circle-opacity": 0.9
  }
}

I.4 Layer Toggles Panel

┌────────────────────────┐
│ LAYERS                 │
│ ┌────────────────────┐ │
│ │ ● Buildings    ☑   │ │
│ │ ◆ Comps        ☑   │ │
│ │ ▲ TIMs         ☐   │ │
│ │ ─ Heatmap      ☐   │ │
│ └────────────────────┘ │
└────────────────────────┘
  • Position: Top-right of map, below zoom controls.
  • Background: rgba(255,255,255,0.92), backdrop-filter: blur(8px).
  • Border: 1px solid color.border.default.
  • Border radius: radius.md (8px).
  • Padding: 12px.
  • Toggle: Mantine Checkbox or Switch, sm size.
  • Entity symbol matches marker shape and color.

I.5 Map Tooltip / Popover

PropertyValue
Backgroundcolor.surface.card (#ffffff)
Border1px solid color.border.default
Border radiusradius.md (8px)
Shadowshadow.dropdown
Padding12px
Max width280px
Titlefont.size.body, weight 600
Body textfont.size.caption, color color.text.secondary
ArrowNone (MapLibre popups without arrows are cleaner)

I.6 Map Performance Rules

  1. Enable clustering at all zoom levels below 14.
  2. Use source.cluster: true with clusterMaxZoom: 14, clusterRadius: 50.
  3. Limit visible features to viewport bounds via MapLibre's built-in frustum culling.
  4. Avoid re-creating map sources on filter change — update source data in place via source.setData().
  5. Use requestAnimationFrame for batch marker updates.
  6. For heatmap layers, downsample to max 5,000 points.
  7. Debounce moveend event handlers by 150ms.
  8. Never force a full map re-render on React state change — use refs for map instance.

J. Table Standards

J.1 Structure

┌────────────────────────────────────────────────────┐
│ Sticky Header                                       │
│ ┌──────┬──────────┬──────────┬──────────┬────────┐ │
│ │  ▲   │ Name ▲   │ Type     │ RBA ▼    │ Status │ │
│ ├──────┼──────────┼──────────┼──────────┼────────┤ │
│ │      │ Park Tw  │ Office   │ 456,000  │ Active │ │
│ │      │ Metro C  │ Industr  │ 234,000  │ Draft  │ │
│ │      │          │          │          │        │ │
│ └──────┴──────────┴──────────┴──────────┴────────┘ │
├────────────────────────────────────────────────────┤
│ Showing 1-25 of 1,234          ← Page 1 of 50 →   │
└────────────────────────────────────────────────────┘

J.2 Header Styling

PropertyValue
Fontfont.size.captionStrong (12px), weight 500, uppercase
Colorcolor.text.secondary
Backgroundcolor.background.default (#f8fafc)
Height40px
Padding8px 12px
Positionsticky, top: 0, z-index: 2 (within table scroll context)
Sort indicatorTabler arrow icon, 14px, color.text.muted (inactive), color.teal.base (active)
BorderBottom: 1px solid color.border.strong

J.3 Cell Alignment Rules

Data TypeAlignmentFormat
Text (name, address)LeftAs-is
Number (SF, price)RightComma-separated, tabular-nums
PercentageRightOne decimal, tabular-nums
CurrencyRight$ prefix, comma-separated, tabular-nums
DateLeftMMM DD, YYYY or MM/DD/YY (compact)
Status / BadgeLeft (or Center)Badge component
ActionsRightIcon buttons or RowActionsMenu

J.4 Row Styling

StateBackgroundBorder
Even rowcolor.surface.card (#ffffff)Bottom: 1px solid color.border.default
Odd rowcolor.background.default (#f8fafc)Bottom: 1px solid color.border.default
Hovercolor.hover.row (#f1f5f9)
Selectedcolor.selected.row (#f0fdfa)
Priority (high)Left: 3px solid color.error.base
Priority (medium)Left: 3px solid color.warning.base

J.5 Pagination

PropertyValue
PositionBelow table, full width
LayoutLeft: "Showing X-Y of Z" (font.size.caption). Right: ← Page N of M →
Button styleActionIcon, outline variant, sm size
DisabledMuted color, not-allowed cursor
Page size selectorOptional Select, sm size, options: 25, 50, 100

J.6 Loading State

  • Use Mantine Skeleton components in place of rows.
  • Skeleton rows: 8 rows matching comfortable row height (44px).
  • Skeleton shimmer: Use Mantine's default skeleton animation (subtle pulse).
  • Header remains visible during loading.

J.7 Empty State

  • Single row spanning all columns.
  • Centered text: font.size.body, color color.text.secondary.
  • Message: "No [entities] found" or "No results match your filters".
  • Optional: Reset filters link below message.

J.8 Error State

  • Alert component above table (not replacing it).
  • color.error.light background, color.error.text text.
  • Retry button inline.

J.9 URL State Sync

All table state syncs to URL search params via TanStack Router:

ParamKeyExample
Pagepage?page=3
Page sizepageSize?pageSize=50
Sort columnsort?sort=rba
Sort directiondir?dir=desc
Searchq?q=park+tower
Filtersfilter_{name}?filter_type=Office

Read from URL on mount, write to URL on change via router.navigate({ search }).

J.10 Virtualization Readiness

For tables exceeding 100 visible rows:

  • Use @tanstack/react-virtual for row virtualization.
  • Maintain a fixed row height (no dynamic content expansion within rows).
  • Measure container height and calculate visible window.
  • Keep sticky header outside virtualized container.
  • Overscan: 5 rows above and below viewport.

K. Chart Standards (ECharts)

K.1 Global ECharts Theme

const PRISM_ECHARTS_THEME = {
  color: [
    '#0d9488', // teal — primary series
    '#1e3a8a', // navy — comparison
    '#7c3aed', // violet
    '#ea580c', // orange
    '#0284c7', // sky
    '#c026d3', // fuchsia
  ],
  backgroundColor: 'transparent',
  textStyle: {
    fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
    color: '#64748b',
    fontSize: 12,
  },
  title: {
    textStyle: {
      fontFamily: "'Inter', sans-serif",
      fontSize: 16,
      fontWeight: 600,
      color: '#111827',
    },
    subtextStyle: {
      fontSize: 12,
      color: '#64748b',
    },
  },
};

K.2 Axis Styling

const axisCommon = {
  axisLine: {
    lineStyle: { color: '#e2e8f0', width: 1 },
  },
  axisTick: {
    lineStyle: { color: '#e2e8f0' },
    length: 4,
  },
  axisLabel: {
    color: '#94a3b8',
    fontSize: 11,
    fontFamily: "'Inter', sans-serif",
    fontVariantNumeric: 'tabular-nums',
  },
  splitLine: {
    lineStyle: { color: '#e2e8f0', type: 'dashed', width: 1 },
  },
  nameTextStyle: {
    color: '#64748b',
    fontSize: 12,
    fontWeight: 500,
  },
};
  • X-axis: Labels aligned to center below ticks.
  • Y-axis: Labels right-aligned, padding 8px from axis line.
  • Grid padding: { top: 40, right: 24, bottom: 40, left: 56 } (adjust left for label width).

K.3 Gridlines

  • Horizontal gridlines only (on Y-axis splitLine).
  • Dashed, 1px, color.chart.grid (#e2e8f0).
  • No vertical gridlines.

K.4 Tooltip

const tooltipStyle = {
  backgroundColor: '#ffffff',
  borderColor: '#e2e8f0',
  borderWidth: 1,
  borderRadius: 8,
  padding: [8, 12],
  textStyle: {
    color: '#111827',
    fontSize: 13,
    fontFamily: "'Inter', sans-serif",
  },
  extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.08);',
};
  • Tooltip trigger: 'axis' for line/bar, 'item' for pie/scatter.
  • Tooltip format: Series color dot + series name + value.
  • Number formatting: comma-separated, 1-2 decimal places max.

K.5 Legend

const legendStyle = {
  type: 'scroll',
  bottom: 0,
  left: 'center',
  textStyle: {
    color: '#64748b',
    fontSize: 12,
    fontFamily: "'Inter', sans-serif",
  },
  icon: 'roundRect',
  itemWidth: 12,
  itemHeight: 8,
  itemGap: 16,
  pageTextStyle: { color: '#94a3b8' },
};
  • Position: Bottom center, below chart.
  • Scrollable when > 6 series.
  • Interactive: click to toggle series visibility.

K.6 Series Color Mapping

SlotColorHexUsage
PrimaryTeal#0d9488Main metric series
ComparisonNavy#1e3a8aBenchmark or comparison
3rdViolet#7c3aedSupplementary
4thOrange#ea580cSupplementary
5thSky#0284c7Supplementary
6thFuchsia#c026d3Supplementary
  • Area fills: 10% opacity of series color.
  • Bar chart: No gradient fills. Solid color at 85% opacity.
  • Line chart: 2px stroke width. No dots except on hover.

K.7 Number Formatting

TypeFormatExample
Square feetComma-separated, 0 decimals456,789
Large SFAbbreviated with suffix1.2M SF
Percentage1 decimal + %8.3%
Currency$ + comma-separated$24.50
Currency (large)$ + abbreviated$1.2M
CountComma-separated1,234
Date (axis)Q1 '24 or Jan '24

K.8 Large Dataset Handling

  • For > 10,000 data points: use ECharts sampling: 'lttb' (Largest Triangle Three Buckets).
  • For > 50,000 data points: server-side aggregation before charting.
  • Enable large: true and largeThreshold: 2000 for scatter plots.
  • Use dataZoom (inside type) for time-series with > 100 x-axis points.

K.9 Loading / Empty States

Loading:

  • Show Mantine Skeleton matching chart card dimensions.
  • Do not show an ECharts spinner.

Empty:

  • Centered within chart area: Icon (Tabler IconChartOff, 32px, color.text.muted) + "No data available" text (font.size.body, color.text.secondary).
  • No ECharts noDataGraphic.

L. Mantine Theme Configuration

L.1 Complete Theme Object

import { createTheme, MantineColorsTuple } from '@mantine/core';

// Custom navy palette for Mantine color system
const navy: MantineColorsTuple = [
  '#eff6ff', // 0
  '#dbeafe', // 1
  '#bfdbfe', // 2
  '#93c5fd', // 3
  '#60a5fa', // 4
  '#3b82f6', // 5
  '#1e3a8a', // 6 — base
  '#1e3a8a', // 7 — base (repeated for primary shade)
  '#172554', // 8
  '#0f172a', // 9
];

// Custom teal palette
const teal: MantineColorsTuple = [
  '#f0fdfa', // 0
  '#ccfbf1', // 1
  '#99f6e4', // 2
  '#5eead4', // 3
  '#2dd4bf', // 4
  '#14b8a6', // 5
  '#0d9488', // 6 — base
  '#0f766e', // 7
  '#115e59', // 8
  '#134e4a', // 9
];

export const prismTheme = createTheme({
  primaryColor: 'teal',
  primaryShade: { light: 6, dark: 7 },

  colors: { navy, teal },

  fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
  fontFamilyMonospace: "'JetBrains Mono', 'Fira Code', monospace",

  headings: {
    fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
    fontWeight: '600',
    sizes: {
      h1: { fontSize: '20px', lineHeight: '1.3' },
      h2: { fontSize: '16px', lineHeight: '1.35' },
      h3: { fontSize: '14px', lineHeight: '1.4' },
      h4: { fontSize: '14px', lineHeight: '1.4' },
    },
  },

  fontSizes: {
    xs: '11px',
    sm: '12px',
    md: '14px',
    lg: '16px',
    xl: '20px',
  },

  spacing: {
    xs: '4px',
    sm: '8px',
    md: '12px',
    lg: '16px',
    xl: '24px',
  },

  radius: {
    xs: '4px',
    sm: '6px',
    md: '8px',
    lg: '10px',
    xl: '12px',
  },

  defaultRadius: 'sm',

  shadows: {
    xs: 'none',
    sm: 'none',
    md: '0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06)',
    lg: '0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)',
    xl: '0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)',
  },

  other: {
    zIndex: {
      mapOverlay: 100,
      mapPopup: 200,
      stickyHeader: 300,
      sidebar: 400,
      topbar: 500,
      dropdown: 600,
      drawer: 700,
      modalBackdrop: 800,
      modal: 801,
      tooltip: 900,
      commandPalette: 950,
      notification: 1000,
    },
  },

  components: {
    Card: {
      defaultProps: {
        withBorder: true,
        shadow: undefined,
        radius: 'md',
        padding: 'lg',
      },
      styles: {
        root: {
          borderColor: 'var(--color-border-default, #e2e8f0)',
        },
      },
    },

    Button: {
      defaultProps: {
        radius: 'sm',
      },
      styles: {
        root: {
          fontWeight: 500,
        },
      },
    },

    TextInput: {
      defaultProps: {
        radius: 'sm',
      },
      styles: {
        input: {
          borderColor: 'var(--color-border-strong, #cbd5e1)',
          '&:focus': {
            borderColor: '#0d9488',
          },
        },
      },
    },

    Select: {
      defaultProps: {
        radius: 'sm',
        comboboxProps: { zIndex: 600 },
      },
    },

    MultiSelect: {
      defaultProps: {
        radius: 'sm',
        comboboxProps: { zIndex: 600 },
      },
    },

    Modal: {
      defaultProps: {
        radius: 'md',
        zIndex: 801,
        overlayProps: { backgroundOpacity: 0.35, blur: 2 },
      },
    },

    Drawer: {
      defaultProps: {
        zIndex: 700,
      },
    },

    Popover: {
      defaultProps: {
        radius: 'md',
        shadow: 'md',
        zIndex: 600,
      },
    },

    Tooltip: {
      defaultProps: {
        radius: 'sm',
        zIndex: 900,
      },
    },

    Table: {
      defaultProps: {
        striped: true,
        highlightOnHover: true,
      },
    },

    Badge: {
      defaultProps: {
        radius: 'xs',
        variant: 'light',
      },
    },

    Tabs: {
      styles: {
        tab: {
          fontWeight: 500,
          fontSize: '14px',
          padding: '8px 16px',
        },
      },
    },

    Skeleton: {
      defaultProps: {
        radius: 'sm',
      },
    },

    Notification: {
      defaultProps: {
        radius: 'md',
      },
    },
  },
});

L.2 CSS Variable Layer

Add to styles.css root:

:root {
  /* Neutrals */
  --color-bg-default: #f8fafc;
  --color-bg-subtle: #f1f5f9;
  --color-bg-muted: #e2e8f0;
  --color-surface-card: #ffffff;
  --color-border-default: #e2e8f0;
  --color-border-strong: #cbd5e1;
  --color-text-primary: #111827;
  --color-text-secondary: #64748b;
  --color-text-muted: #94a3b8;

  /* Structural */
  --color-navy-base: #1e3a8a;
  --color-navy-dark: #172554;
  --color-navy-light: #dbeafe;
  --color-navy-text: #eff6ff;

  /* Action */
  --color-teal-base: #0d9488;
  --color-teal-hover: #0f766e;
  --color-teal-pressed: #115e59;
  --color-teal-light: #ccfbf1;
  --color-teal-subtle: #f0fdfa;

  /* Semantic */
  --color-success: #059669;
  --color-warning: #d97706;
  --color-error: #dc2626;
  --color-info: #2563eb;

  /* Interaction */
  --color-hover-row: #f1f5f9;
  --color-selected-row: #f0fdfa;
  --color-focus-ring: 0 0 0 2px #ffffff, 0 0 0 4px #0d9488;

  /* Typography */
  --font-tabular: 'tnum' 1, 'lnum' 1;
}

M. Compact Mode Implementation Strategy

M.1 Architecture

DensityProvider (React Context)
├── value: "comfortable" | "compact"
├── toggle: () => void
├── persisted to localStorage per-page
└── sets data-density attribute on wrapper div

M.2 File Structure

shared/
  ui/
    DensityProvider.tsx     — Context provider + hook
    DensityToggle.tsx       — SegmentedControl toggle component
  theme/
    density.css             — All density-responsive CSS overrides

M.3 DensityProvider Implementation

// DensityProvider.tsx
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';

type Density = 'comfortable' | 'compact';

interface DensityContextValue {
  density: Density;
  setDensity: (d: Density) => void;
}

const DensityContext = createContext<DensityContextValue>({
  density: 'comfortable',
  setDensity: () => {},
});

export function useDensity() {
  return useContext(DensityContext);
}

export function DensityProvider({ pageKey, children }: { pageKey: string; children: ReactNode }) {
  const storageKey = `prism:density:${pageKey}`;
  const [density, setDensityState] = useState<Density>(
    () => (localStorage.getItem(storageKey) as Density) || 'comfortable'
  );

  const setDensity = useCallback((d: Density) => {
    setDensityState(d);
    localStorage.setItem(storageKey, d);
  }, [storageKey]);

  return (
    <DensityContext.Provider value={{ density, setDensity }}>
      <div data-density={density}>
        {children}
      </div>
    </DensityContext.Provider>
  );
}

M.4 DensityToggle Component

// DensityToggle.tsx
import { SegmentedControl } from '@mantine/core';
import { IconLayoutDistributeVertical, IconLayoutAlignMiddle } from '@tabler/icons-react';
import { useDensity } from './DensityProvider';

export function DensityToggle() {
  const { density, setDensity } = useDensity();

  return (
    <SegmentedControl
      size="xs"
      value={density}
      onChange={(v) => setDensity(v as 'comfortable' | 'compact')}
      data={[
        { label: 'Comfortable', value: 'comfortable' },
        { label: 'Compact', value: 'compact' },
      ]}
    />
  );
}

M.5 Pages That Support Density Toggle

  • BuildingsExplorePage
  • CompsExplorePage
  • TimsExplorePage
  • OperationsQueuePage
  • OperationsSavedViewsPage (if table-based)

Wrap these pages in <DensityProvider pageKey="buildings">. Add <DensityToggle /> to their DataTableToolbar.


N. Reusable Component Structure

N.1 Current Components (Keep)

ComponentLocationStatus
PageHeadershared/ui/PageHeader.tsxKeep, no changes needed
FormSectionBlockshared/ui/FormSectionBlock.tsxKeep
DataTableToolbarshared/ui/DataTableToolbar.tsxKeep, add density toggle slot
StatBarshared/ui/StatBar.tsxKeep
RowActionsMenushared/ui/RowActionsMenu.tsxKeep
LookupSelectshared/ui/LookupSelect.tsxKeep
QueryFeedbackshared/ui/QueryFeedback.tsxKeep
TableEmptyStateRowshared/ui/TableEmptyStateRow.tsxKeep
AddressMapPickershared/ui/AddressMapPicker.tsxKeep
CommandLaunchershared/ui/CommandLauncher.tsxKeep

N.2 New Components (Add)

ComponentLocationPurpose
DensityProvidershared/ui/DensityProvider.tsxDensity context + wrapper
DensityToggleshared/ui/DensityToggle.tsxDensity switch control
KpiCardshared/ui/KpiCard.tsxStandardized KPI display
ConfirmActionshared/ui/ConfirmAction.tsxDestructive action popover
EntityDrawershared/ui/EntityDrawer.tsxStandard create/edit drawer shell
ChartCardshared/ui/ChartCard.tsxChart wrapper with title + loading/empty
MapContainershared/ui/MapContainer.tsxStandard MapLibre container with controls
FilterBarshared/ui/FilterBar.tsxCollapsible filter controls row

N.3 Component API Sketches

KpiCard

interface KpiCardProps {
  label: string;
  value: string | number;
  delta?: { value: number; direction: 'up' | 'down' | 'flat' };
  format?: 'number' | 'percent' | 'currency' | 'sf';
  linkTo?: string;
  loading?: boolean;
}

EntityDrawer

interface EntityDrawerProps {
  opened: boolean;
  onClose: () => void;
  title: string;
  width?: number; // default 480
  children: ReactNode;
  footer?: ReactNode; // sticky footer with Cancel + Save
  loading?: boolean;
}

ChartCard

interface ChartCardProps {
  title: string;
  subtitle?: string;
  height?: number; // default 300
  loading?: boolean;
  empty?: boolean;
  emptyMessage?: string;
  children: ReactNode; // ECharts component
  actions?: ReactNode; // export button etc
}

ConfirmAction

interface ConfirmActionProps {
  title: string;
  message: string;
  confirmLabel: string; // e.g., "Delete"
  onConfirm: () => void;
  variant?: 'danger' | 'warning'; // default danger
  children: ReactNode; // trigger element
}

O. UI Consistency Checklist

O.1 Color Usage

  • Page backgrounds use color.background.default (#f8fafc)
  • Cards use color.surface.card with 1px solid color.border.default
  • No card shadows (borders only)
  • Primary buttons use color.teal.base
  • Sidebar uses color.navy.base background
  • Active nav items have teal left accent
  • Destructive actions use color.error.base
  • No competing saturated colors beyond navy (structural) and teal (action)
  • Semantic colors used only for their intended purpose

O.2 Typography

  • Font: Inter loaded and applied globally
  • Page titles: 20px / 600 weight
  • Section titles: 16px / 600 weight
  • Body text: 14px / 400 weight
  • Captions: 12px / 400 weight
  • Table headers: 12px / 500 weight / uppercase
  • KPI values: 28px / 700 weight / tabular-nums
  • All numeric displays use tabular-nums
  • No italic usage in UI text
  • No decorative typography

O.3 Spacing & Layout

  • 8px base grid respected
  • Page padding: 20px
  • Card padding: 16px
  • Stack gaps use spacing tokens (4, 8, 12, 16, 24, 32)
  • No spacing values outside the token set
  • Consistent gutter: 16px
  • Tables full-width within containers

O.4 Interactions

  • All focus states use the standard teal focus ring
  • Focus visible only on keyboard navigation (:focus-visible)
  • Hover states are subtle background changes, never transforms
  • No hover shadows or lifts on cards
  • Disabled states use designated disabled tokens
  • Destructive actions require confirmation
  • Form validation shown on blur or submit, not on keystroke

O.5 Component Patterns

  • Create/edit actions use right-side Drawer (not modal)
  • Delete confirmations use centered Modal
  • All tables have loading skeleton, empty state, and error state
  • All charts have loading skeleton and empty state
  • Pagination uses "Showing X-Y of Z" + prev/next buttons
  • Search inputs are debounced (200-250ms)
  • Filter state synced to URL params
  • Density toggle present on Explore and Operations pages

O.6 Maps

  • Map uses desaturated basemap
  • Entity types distinguishable by shape AND color
  • Clusters use navy fill
  • Selected features use highlight yellow
  • Floating panels have frosted glass treatment
  • Map instance managed via ref, not React state
  • Clustering enabled below zoom 14
  • moveend handlers debounced

O.7 Charts

  • Primary series uses teal
  • Comparison series uses navy
  • No rainbow palettes
  • Horizontal gridlines only, dashed
  • Legend at bottom center
  • Tooltip has white background, border, shadow
  • Axis labels use tabular-nums
  • Large datasets use sampling

O.8 Accessibility

  • All text meets WCAG AA contrast ratio (4.5:1 normal, 3:1 large)
  • Interactive elements have visible focus indicators
  • Icon-only buttons have aria-label
  • Form inputs have associated labels
  • Error messages are announced (aria-live or role="alert")
  • Keyboard navigation works for all interactive flows
  • Color is never the sole differentiator for information

O.9 WCAG AA Contrast Verification

PairForegroundBackgroundRatioPass
Primary text on page bg#111827#f8fafc15.4:1Yes
Secondary text on page bg#64748b#f8fafc4.9:1Yes
Primary text on card#111827#ffffff17.3:1Yes
Secondary text on card#64748b#ffffff5.5:1Yes
Muted text on card#94a3b8#ffffff3.3:1Fail (large text only)
Teal on white#0d9488#ffffff4.5:1Yes (borderline)
Navy text on white#1e3a8a#ffffff9.4:1Yes
White on navy#eff6ff#1e3a8a8.7:1Yes
Error on white#dc2626#ffffff4.6:1Yes
Success on white#059669#ffffff4.6:1Yes

Note: color.text.muted (#94a3b8) fails AA for small text. Use only for:

  • Placeholder text (exempt per WCAG)
  • Decorative/supplementary text where adjacent high-contrast text carries the meaning
  • Large text (18px+ or 14px+ bold)

P. File Summary

All specification sections mapped to implementation files:

Spec SectionImplementation File(s)
A. Design Tokensshared/theme/prismColors.ts, app/styles.css (:root vars)
B. Typographyapp/providers.tsx (Mantine theme), app/styles.css (font import)
C. Spacing/Densityapp/providers.tsx (theme), shared/ui/DensityProvider.tsx, shared/theme/density.css
D. Layout GridMantine Grid/SimpleGrid usage in page components
E. Z-Indexapp/providers.tsx (theme other.zIndex), app/styles.css
F. Shellshared/ui/AppShellLayout.tsx, app/styles.css
G. Interactionsapp/styles.css (global states), app/providers.tsx (component defaults)
H. Page PatternsIndividual page components in modules/*/pages/
I. Map Standardsshared/theme/prismColors.ts (MAP_COLORS), map components
J. Table Standardsapp/styles.css (.prism-table), shared/ui/TableEmptyStateRow.tsx
K. Chart Standardsshared/theme/prismColors.ts (ANALYTICS_COLORS), analytics pages
L. Mantine Themeapp/providers.tsx
M. Compact Modeshared/ui/DensityProvider.tsx, shared/ui/DensityToggle.tsx, shared/theme/density.css
N. Componentsshared/ui/*.tsx