Frontend Specs
RankFlow AI — Frontend Specification
Document Version: 1.0.0
docs/specs/frontend/frontend-spec-01-design-system.mdOn this page
- Section 1: Design System & Global Foundations
- 1.1 Document Overview
- 1.2 Design Token System
- 1.2.1 Color Palette
- 1.2.2 Typography
- 1.2.3 Spacing Scale
- 1.2.4 Border & Radius
- 1.2.5 Shadows
- 1.2.6 Motion
- 1.2.7 Z-Index Scale
- 1.3 Global Component Patterns
- 1.3.1 Button
- 1.3.2 Input
- 1.3.3 Card
- 1.3.4 Table
- 1.3.5 Modal / Dialog
- 1.3.6 Toast / Notification
- 1.3.7 Badge / Status Tag
- 1.3.8 Sidebar Navigation
- 1.4 Auth State Management
- 1.4.1 Session Architecture
- 1.4.2 tRPC Context
- 1.4.3 Role-Based Access Control (RBAC)
- 1.4.4 Data Fetching Patterns
- 1.4.5 Error Handling & Loading States
- 1.5 Responsive Design
- 1.5.1 Breakpoints
- 1.5.2 Layout Rules
- 1.5.3 Touch Targets
- 1.6 Accessibility Requirements
- 1.6.1 WCAG 2.1 AA Compliance
- 1.6.2 ARIA Patterns
- 1.6.3 Form Accessibility
- 1.7 Representative Widget: KPI Card (Depth Validation)
- 1.7.1 KPI Card — Specification
- 1.8 Icon System
- 1.9 Data Visualization (Charts)
- 1.10 Form Validation Patterns
- 1.11 File Upload Patterns
- 1.12 Search & Filter Patterns
- 1.13 Summary
Section 1: Design System & Global Foundations#
Document Version: 1.0.0
Date: 2026-01-15
Owner: Frontend Engineering
Scope: All client-facing and admin-facing UI components, screens, and interactions
Format: Markdown (.md) — primary deliverable for version control and diffing
Tech Stack: Next.js 14 (App Router), React 18, TypeScript, Tailwind CSS, tRPC, React Query (TanStack Query), Zod, Better Auth, Recharts, date-fns, Lucide React
1.1 Document Overview#
This specification acts as the single source of truth for all frontend implementation. Every screen, widget, pop-up, interaction flow, and data wire is documented with exact backend endpoint names, fetch frequencies, display patterns, and manipulation rules. Frontend coding agents must follow this document with zero ambiguity.
Critical constraints (non-negotiable):
- NO bold font weight. Use
font-weight: 500(Medium) as the heaviest weight. Suisse Intl Medium is the max. - Ember Orange
#EC652Bis ONLY for: primary action buttons, CTA links, focus rings, and positive outcome indicators. Never for text, backgrounds, error states, or status badges. - Page max-width:
1200pxcentered. Canvas background: Paper White#F6F6F8. - Sidebar:
240pxwide, Deep Ink#011821background, always visible on desktop, collapses to hamburger on mobile. - Section gap:
80pxbetween major sections. - Card radius:
8px(border-radius-lg). - Medical compliance: All content for medical clients (CLINIC/HOSPITAL/DOCTOR) goes through 24h approval queue. No exceptions.
1.2 Design Token System#
All tokens are consumed via Tailwind CSS config (tailwind.config.ts) and exposed as CSS custom properties in :root.
1.2.1 Color Palette#
| Token Name | Hex Value | Usage | Restriction |
|---|---|---|---|
paper-white |
#F6F6F8 |
Primary canvas, page background | Never use for text on light surfaces |
card-white |
#FFFFFF |
Cards, dialogs, inputs, elevated surfaces | Default surface for all cards |
deep-ink |
#011821 |
Primary text, dark surfaces, sidebar bg | Never use #000000 |
carbon |
#12161E |
Secondary dark panels, admin dense tables | Technical dark surfaces |
deep-indigo |
#111A4A |
Charts, data series, dark accents | Supporting only |
midnight-teal |
#023247 |
Info states, technical panels, hover bg | Primary button hover |
ember-orange |
#EC652B |
CTA ONLY — primary action, focus, booked | NEVER for text, backgrounds, or status |
forest-teal |
#167E6C |
Success, verified, live states, positive data | Success badges, checkmarks |
sky-blue |
#7EA7E9 |
Informational data series, secondary charts | Data viz only |
pale-cyan |
#C1E8EF |
Info backgrounds, selected states, soft alerts | Light backgrounds only |
mint |
#44B48B |
Secondary positive data, illustration | Supporting green |
lavender |
#9F7AEE |
Secondary data series only | Data viz only |
mist |
#E3E4E8 |
1px borders, separators, rules | All borders default |
fog |
#A9ACB6 |
Disabled controls, non-essential decoration | Disabled states only |
slate |
#7C7F88 |
Tertiary labels, muted metadata | Captions, timestamps |
graphite |
#3B3E47 |
Secondary text, icons, body text | Body text on light surfaces |
error-red |
#C2442A |
Destructive actions, errors, failed states | Cancel, delete, error alerts |
Semantic aliases (use these in components, not raw hex):
| Semantic Token | Maps To | Usage |
|---|---|---|
text-primary |
deep-ink |
Headings, primary body text |
text-secondary |
graphite |
Body text, descriptions |
text-tertiary |
slate |
Timestamps, metadata, captions |
text-disabled |
fog |
Disabled labels, placeholder text |
text-inverse |
paper-white |
Text on dark backgrounds |
surface-canvas |
paper-white |
Page background |
surface-card |
card-white |
Card, modal, input backgrounds |
surface-dark |
deep-ink |
Sidebar, dark panels |
border-default |
mist |
All default borders |
action-primary |
deep-ink |
Primary button bg |
action-accent |
ember-orange |
CTA button bg only |
success |
forest-teal |
Success states |
warning |
ember-orange |
Warning states (same as accent) |
error |
error-red |
Error states |
info |
midnight-teal |
Info states |
1.2.2 Typography#
Font family: Suisse Intl primary. Fallback stack: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif.
Monospace: Suisse Intl Mono → 'SFMono-Regular', Menlo, Monaco, Consolas, monospace.
| Token | Size | Line Height | Letter Spacing | Weight | Usage |
|---|---|---|---|---|---|
display |
60px | 66px | -0.03em | 500 | Hero headlines only |
heading-lg |
40px | 44px | -0.02em | 500 | Page titles, section headers |
heading |
28px | 36px | -0.02em | 500 | Card titles, modal headers |
heading-sm |
24px | 32px | -0.02em | 500 | Sub-section headers |
subheading |
20px | 28px | -0.01em | 500 | Card subtitles, list headers |
body-lg |
18px | 26px | -0.01em | 400 | Lead paragraphs, descriptions |
body |
16px | 24px | 0 | 400 | Default body text |
body-sm |
14px | 21px | 0 | 400 | Secondary text, form labels |
caption |
12px | 18px | 0.01em | 400 | Timestamps, badges, metadata |
Weight rules:
400(Regular): All body text, descriptions, labels, inputs.500(Medium): Headings, buttons, navigation, active states, emphasis.- NO
600or700on any text.font-semiboldandfont-boldare banned. 300(Light): Optional for hero subtext or large display numbers.
1.2.3 Spacing Scale#
Base unit: 4px. All spacing values are multiples of 4px.
| Token | Value | Usage |
|---|---|---|
space-1 |
4px | Micro gaps, icon padding |
space-2 |
8px | Tight element gaps, inline spacing |
space-3 |
12px | Default element gap, form field spacing |
space-4 |
16px | Card internal padding (compact) |
space-5 |
20px | Button padding, input padding |
space-6 |
24px | Standard card padding, modal padding |
space-8 |
32px | Section internal padding, grid gaps |
space-10 |
40px | Between card groups, form sections |
space-12 |
48px | Between major UI regions |
space-16 |
64px | Between page sections |
space-20 |
80px | Between major page sections (hero to content) |
space-24 |
96px | Large page spacing |
Layout tokens:
page-max-width: 1200px (centered withmx-auto).section-gap: 80px (between major sections).card-padding: 24px (internal card padding).element-gap: 12px (between sibling elements in a card).
1.2.4 Border & Radius#
| Token | Value | Usage |
|---|---|---|
radius-none |
0 | Tables, data grids |
radius-sm |
2px | Small tags, compact badges |
radius-lg |
8px | Cards, buttons, inputs, modals — default |
radius-xl |
12px | Large cards, feature sections |
radius-full |
9999px | Pills, avatars, status badges |
border-default |
1px | All default borders (mist color) |
border-strong |
2px | Focus states, active selections |
1.2.5 Shadows#
| Token | Value | Usage |
|---|---|---|
shadow-subtle |
0 1px 3px rgba(17,26,74,0.10), 0 1px 0 rgba(17,26,74,0.05), inset 0 1px 0 rgba(255,255,255,0.50), inset 0 1px 4px rgba(255,255,255,0.50) |
Card default |
shadow-outline |
0 0 0 1px rgba(87,90,100,0.12) |
Subtle card borders |
shadow-sm |
0 4px 8px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.10), 0 1px 1px rgba(0,0,0,0.10) |
Elevated cards, dropdowns |
shadow-xl |
0 40px 32px rgba(0,0,0,0.02), 0 22px 18px rgba(0,0,0,0.03), 0 12px 10px rgba(0,0,0,0.03), 0 7px 5px rgba(0,0,0,0.04), 0 3px 2px rgba(0,0,0,0.07) |
Modals, dialogs |
shadow-focus |
0 0 0 3px rgba(236,101,43,0.28) |
Focus rings (ember-orange) |
1.2.6 Motion#
| Token | Value | Usage |
|---|---|---|
duration-instant |
100ms | Hover color changes, opacity |
duration-fast |
150ms | Button presses, icon toggles |
duration-base |
200ms | Card transitions, dropdowns, modals |
duration-slow |
300ms | Page transitions, sidebar collapse |
easing-standard |
cubic-bezier(0.2, 0, 0, 1) |
Default easing |
easing-enter |
cubic-bezier(0, 0, 0, 1) |
Entering elements |
easing-exit |
cubic-bezier(0.3, 0, 1, 1) |
Exiting elements |
Animation rules:
- All transitions use
duration-base(200ms) unless specified otherwise. - Modal enter:
duration-slow+easing-enter. Exit:duration-fast+easing-exit. - Sidebar collapse:
duration-slow+easing-standard. - Skeleton shimmer:
duration-slowinfinite loop,translateXanimation. - Toast enter: slide-in from bottom + fade,
duration-base. Exit: fade-out,duration-fast. - Reduced motion: respect
prefers-reduced-motion: reduce— disable all non-essential animations.
1.2.7 Z-Index Scale#
| Token | Value | Usage |
|---|---|---|
z-base |
0 | Default stacking |
z-dropdown |
100 | Select menus, dropdowns |
z-sticky |
200 | Sticky headers, sidebars |
z-fixed |
300 | Fixed navbars |
z-modal-backdrop |
400 | Modal overlay |
z-modal |
500 | Modal content |
z-popover |
600 | Popovers, tooltips |
z-tooltip |
700 | Tooltips (above popovers) |
z-toast |
800 | Toasts (topmost) |
1.3 Global Component Patterns#
Every component documented below follows the 10-point specification template. This section defines the base patterns; all screen-specific components inherit from these.
1.3.1 Button#
Variants:
| Variant | Background | Text | Border | Hover BG | Hover Border | Usage |
|---|---|---|---|---|---|---|
| Primary | deep-ink |
paper-white |
none | midnight-teal |
none | Main action on page |
| Accent | ember-orange |
deep-ink |
none | #D95A23 (darkened 10%) |
none | CTA only — conversion actions |
| Secondary | card-white |
deep-ink |
mist 1px |
rgba(1,24,33,0.04) |
fog |
Alternative actions |
| Ghost | transparent | deep-ink |
mist 1px |
rgba(1,24,33,0.04) |
fog |
Low-emphasis actions |
| Danger | error-red |
paper-white |
none | #A8321F (darkened 15%) |
none | Destructive actions |
| Link | transparent | midnight-teal |
none | transparent | none | Text links, underline on hover |
Specs:
- Padding:
10px 16px(compact:8px 12px). - Radius:
8px(radius-lg). - Font:
body-sm(14px), weight 500. - Icon + text: icon left,
space-2(8px) gap. Icon size:size-md(20px). - Loading state: spinner replaces icon, disabled opacity 0.6, cursor
not-allowed. - Disabled state:
fogbackground,slatetext, no hover effect. - Focus:
shadow-focusring (ember-orange, 3px). - Width: auto (hug content) by default. Full-width variant for mobile.
States:
- Default → Hover (150ms, color transition).
- Pressed → Active (scale 0.98, 100ms).
- Loading → Spinner rotation, disabled pointer events.
- Disabled → Opacity 0.6, no interaction.
1.3.2 Input#
Types: text, email, password, number, tel, url, textarea, select, date, file.
Specs:
- Background:
card-white. - Border:
mist1px, radius8px. - Padding:
10px 12px. - Font:
body(16px), weight 400, colortext-primary. - Placeholder:
graphite, weight 400. - Focus: border
ember-orange,shadow-focusring. - Error: border
error-red, error message below incaption(12px), colorerror-red. - Disabled: background
paper-white, textfog, bordermist. - Label:
body-sm(14px), weight 500, colortext-primary,space-1(4px) margin-bottom. - Helper text:
caption(12px), colortext-tertiary,space-1margin-top. - Icon left:
space-3(12px) left padding increase, icon colorslate.
Validation:
- Real-time on blur (not on every keystroke to reduce noise).
- Zod schema validation via tRPC error responses.
- Error state persists until corrected.
1.3.3 Card#
Variants: metric, content, action, data.
Specs:
- Background:
card-white. - Border:
mist1px orshadow-outline(no border + outline shadow). - Radius:
8px. - Padding:
24px(space-6). - Shadow:
shadow-subtle(default),shadow-sm(elevated/hover). - Hover:
shadow-sm,translateY(-1px), 200ms transition. - Internal layout: header (title + action), body (content), footer (metadata/actions). Gap:
space-3(12px) between regions.
Metric Card (special):
- Header: label (
caption,text-tertiary), optional trend badge. - Body: value (
headingorheading-lg, weight 500), change indicator (arrow + %, color-coded). - Footer: sparkline (mini line chart, 30px height) or comparison text.
- Padding:
20px(compact).
1.3.4 Table#
Specs:
- Header: background
paper-white, textcaption(12px), weight 500, uppercase, colortext-tertiary, border-bottommist1px. - Row: background
card-white, border-bottommist1px, hoverrow-hover(rgba(1,24,33,0.02)). - Cell padding:
12px 16px. - Radius:
8pxon outer corners (container), rows have no internal radius. - Sortable header: clickable, sort icon (arrow up/down), active sort:
text-primary+ icon colorember-orange. - Pagination: below table,
space-4(16px) margin-top. Page size: 20/50/100. - Empty state: centered icon + text + CTA button,
space-12(48px) padding. - Loading: skeleton rows (5 rows, pulsing
paper-whitetomist). - Selection: checkbox leftmost column, batch action bar appears above table when items selected.
1.3.5 Modal / Dialog#
Specs:
- Backdrop:
rgba(1, 24, 33, 0.48),z-modal-backdrop, click to close (unless critical). - Container:
card-white,radius-lg,shadow-xl, max-width:560px(small),720px(medium),960px(large),1200px(xl). - Padding:
24px(space-6). - Header: title (
heading-sm, 24px, weight 500), close button (X icon, top-right). - Body: scrollable if content exceeds
70vh. - Footer: action buttons (right-aligned, primary on right),
space-4(16px) gap. - Animation: enter (fade-in + scale 0.95→1, 200ms), exit (fade-out + scale 1→0.95, 150ms).
- Focus trap: first focusable element focused on open. Return focus on close.
- Scroll lock: body scroll disabled when modal open.
Types:
- Confirmation: Title, description, primary action (danger or accent), secondary cancel.
- Form: Title, form fields, validation, primary submit, secondary cancel.
- Detail: Title, read-only content, close button, optional edit action.
- Slide-over: Right-aligned panel (max-width 480px), used for filters, details, creation flows. Full height, slide-in from right (300ms).
1.3.6 Toast / Notification#
Specs:
- Position: bottom-right (desktop), bottom-center (mobile),
z-toast. - Container:
card-white,radius-lg,shadow-sm, max-width400px. - Padding:
12px 16px. - Icon left: status icon (check, alert, info, error), color-matched.
- Content: title (
body-sm, weight 500), description (caption, optional). - Close button: X icon, right side.
- Auto-dismiss: success 3000ms, info 5000ms, warning 5000ms, error 8000ms (or manual close).
- Animation: enter (slide-up + fade, 200ms), exit (fade-out, 150ms).
- Max stacked: 5 toasts. Newest at bottom.
- Progress bar: thin line at bottom, shrinks during auto-dismiss timer.
Status colors:
- Success: icon
forest-teal, border-leftforest-teal3px. - Warning: icon
ember-orange, border-leftember-orange3px. - Error: icon
error-red, border-lefterror-red3px. - Info: icon
midnight-teal, border-leftmidnight-teal3px.
1.3.7 Badge / Status Tag#
Specs:
- Radius:
full(pill shape). - Padding:
4px 12px. - Font:
caption(12px), weight 500. - Icon optional: left of text,
space-1gap.
Variants:
| Status | Background | Text | Icon | Usage |
|---|---|---|---|---|
| Success | rgba(22,126,108,0.12) |
forest-teal |
Check circle | Completed, active, verified |
| Warning | rgba(236,101,43,0.12) |
ember-orange |
Alert triangle | Pending, warning, needs attention |
| Error | rgba(194,68,42,0.12) |
error-red |
X circle | Failed, error, rejected |
| Info | rgba(2,50,71,0.12) |
midnight-teal |
Info circle | Info, draft, scheduled |
| Neutral | rgba(1,24,33,0.06) |
graphite |
Dot | Default, inactive |
| Processing | rgba(159,122,238,0.12) |
lavender |
Loader | Running, syncing, generating |
1.3.8 Sidebar Navigation#
Specs:
- Width:
240px(desktop),64px(collapsed), full-width overlay (mobile). - Background:
deep-ink(#011821). - Text:
paper-whiteprimary,fogmuted. - Active item:
rgba(246,246,248,0.12)background, left borderember-orange3px, textpaper-whiteweight 500. - Hover item:
rgba(246,246,248,0.08)background, 150ms transition. - Item padding:
10px 16px. - Icon:
size-md(20px), left of label,space-3gap. - Group header:
caption(12px), uppercase,fog,space-4margin-top. - Collapse toggle: bottom of sidebar, chevron icon, 200ms rotation animation.
- Mobile: hamburger menu in top nav, slide-in from left, backdrop overlay.
- Logo area: top of sidebar,
space-6padding, logo + app name.
Navigation items (client):
- Home (
/dashboard) — LayoutDashboard icon - GBP (
/dashboard/gbp) — MapPin icon - Social (
/dashboard/social) — Share2 icon - Citations (
/dashboard/citations) — Link icon - Site (
/dashboard/site) — Globe icon - Content (
/dashboard/content) — FileText icon - Leads (
/dashboard/leads) — Users icon - Reports (
/dashboard/reports) — BarChart3 icon - Settings (
/dashboard/settings) — Settings icon
Navigation items (admin):
- KPI Overview (
/admin/kpi) — Activity icon - Clients (
/admin/clients) — Users icon - Infrastructure (
/admin/infrastructure) — Server icon - Content (
/admin/content) — FileText icon - Billing (
/admin/billing) — CreditCard icon - Reports (
/admin/reports) — BarChart3 icon
1.4 Auth State Management#
1.4.1 Session Architecture#
Better Auth integration:
- Session stored in HTTP-only cookie (
__session), not localStorage. - Session refresh: automatic via Better Auth middleware, 30-day expiry, sliding window.
- Role decoded from JWT claim:
role: "ADMIN" | "CLIENT" | "EDITOR" | "VIEWER". - User object shape:
interface User { id: string; email: string; name: string; role: "ADMIN" | "CLIENT" | "EDITOR" | "VIEWER"; practiceId: string | null; emailVerified: boolean; mfaEnabled: boolean; image?: string; }
1.4.2 tRPC Context#
- All tRPC queries/mutations include session via
ctx.session. - Auth middleware stack (order matters):
authMiddleware— validates session, attaches user to ctx.roleMiddleware— checks role against route requirements.auditMiddleware— logs action with user ID, timestamp, route.impersonationMiddleware— ifimpersonating: truein session, restricts to read-only for most operations.
1.4.3 Role-Based Access Control (RBAC)#
| Role | Access Level | Description |
|---|---|---|
| ADMIN | Full system access | All routes, all operations, impersonation, system config |
| CLIENT | Practice owner | Full dashboard access, billing, team management, all actions |
| EDITOR | Content manager | All dashboard except billing/team, can approve/reject content |
| VIEWER | Read-only observer | View all data, no actions, no edits, no approvals |
Route guards:
/dashboard/*: CLIENT, EDITOR, VIEWER allowed. ADMIN redirected to/admin./admin/*: ADMIN only. Non-ADMIN get 403 redirect to/dashboard./onboarding: Only during onboarding flow. Post-completion redirect to/dashboard./: Public (marketing). Auth state shows "Dashboard" or "Sign In" in nav./login,/signup: Public. Authenticated users redirected to/dashboard(or/adminfor ADMIN).
Permission checks (client-side):
const canEdit = user.role === "CLIENT" || user.role === "EDITOR";
const canManageBilling = user.role === "CLIENT";
const canApprove = user.role === "CLIENT" || user.role === "EDITOR";
const isAdmin = user.role === "ADMIN";
UI enforcement:
- Hidden buttons/links for disallowed actions (not disabled — hidden to reduce clutter).
- Disabled state only used when action is conditionally unavailable (e.g., "Approve" disabled if already approved).
- Admin impersonation: red banner at top of all pages:
Impersonating [Client Name] — [time remaining]. All actions logged withimpersonating: true. 2-hour timeout. Exit impersonation button (X) in banner.
1.4.4 Data Fetching Patterns#
tRPC Client Setup:
- React Query (
@tanstack/react-query) provider wraps app. - Default query options:
const defaultQueryOptions = { staleTime: 60 * 1000, // 60 seconds cacheTime: 5 * 60 * 1000, // 5 minutes refetchOnWindowFocus: true, refetchOnReconnect: true, retry: 2, // 2 retries on failure retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), };
Query Categories by Refresh Rate:
| Category | Examples | StaleTime | CacheTime | Refetch Interval | Polling |
|---|---|---|---|---|---|
| Real-time | Job status, queue depth | 0 | 1 min | 10 seconds | Yes |
| Frequent | KPIs, reviews, leads, content queue | 60s | 5 min | 60 seconds | Yes |
| Standard | Charts, posts, pages, redirects, prompts | 5 min | 15 min | 5 minutes | Yes |
| Slow | NAP health, site health, citation health, reports | 1 hour | 4 hours | 1 hour | Yes |
| Static | Plans, templates, help articles | 24 hours | 24 hours | None | No |
| On-demand | Search, filters, export | 0 | 0 | None | No |
Mutation Patterns:
- Optimistic updates: update cache immediately, rollback on error.
- Invalidation:
utils.invalidate()on success to trigger refetch. - Toast on success/error with action name.
- Loading state: button spinner, form disabled, overlay blocker for heavy operations.
Prefetching:
- Route prefetch: on hover of sidebar link, prefetch query for target route.
- Dashboard prefetch: on login, prefetch all dashboard queries in background.
- Modal prefetch: on hover of action button that opens modal, prefetch modal data.
1.4.5 Error Handling & Loading States#
Error Boundary:
- Top-level error boundary catches all React errors.
- Fallback UI:
deep-inkbackground,paper-whitetext, error message, "Reload page" button, "Contact support" link. - Logged to Sentry (if configured) with user ID, route, error stack.
Query Error States:
- Initial load error: Full-page error state. Icon (alert triangle), title ("Failed to load data"), description (error message from tRPC), "Retry" button (primary), "Contact support" link.
- Background refetch error: Toast notification (error type), no UI change. Retry automatically on next interval.
- Partial data error: Show available data + inline error banner at top of affected section. "Some data could not be refreshed. Retrying..." with spinner.
Loading States:
- Skeleton: Pulsing
paper-whitetomistanimation. Used for cards, tables, charts. Match layout of final content (same number of lines, same heights). - Spinner: Centered rotating spinner (Lucide
Loader2icon,forest-tealorember-orange). Used for buttons, inline loading, small areas. - Progress bar: Linear progress bar (height 3px,
ember-orangefill,misttrack). Used for file uploads, long operations, onboarding steps. - Empty state: Icon + title + description + CTA. Used when data is loaded but empty. Example: "No reviews yet" + "Connect your GBP" button.
Retry Patterns:
- Automatic: 2 retries with exponential backoff (1s, 2s, 4s).
- Manual: "Retry" button on error state. Trigger
refetch()on click. - Background: Silent retry on next polling interval.
1.5 Responsive Design#
1.5.1 Breakpoints#
| Name | Width | Layout Changes |
|---|---|---|
mobile |
< 640px | Single column, stacked nav, full-width cards, hamburger menu, bottom sheet modals |
tablet |
640px – 1023px | 2-column grids, collapsible sidebar (overlay), modals centered |
desktop |
1024px – 1279px | Full sidebar visible, 3-column grids, modals centered |
wide |
1280px+ | Max-width 1200px container, 4-column grids, full sidebar, slide-overs |
1.5.2 Layout Rules#
- Container:
max-width: 1200px,margin: 0 auto,padding: 0 24px(desktop),0 16px(mobile). - Sidebar: Always visible on
desktop+. Collapsed to64pxicon-only ontablet. Hidden (hamburger) onmobile. - Main content:
margin-left: 240px(sidebar width) ondesktop+. Full width onmobile/tablet. - Card grid: 4 columns (
desktop), 2 columns (tablet), 1 column (mobile). Gap:space-4(16px). - Table: Horizontal scroll on
mobilewith sticky first column. Card-based list onmobile(optional). - Modal: Full-screen on
mobile(slide-up from bottom). Centered ontablet+. Max-width: 100% on mobile, 560px on tablet, 720px on desktop. - Form: Single column on
mobile/tablet. Two-column ondesktop(with labels left-aligned).
1.5.3 Touch Targets#
- Minimum touch target:
44px × 44px(Apple HIG) /48px × 48px(Material). - Button height: minimum
40px(compact),44px(standard). - Icon buttons:
40px × 40pxcontainer, icon20pxor24px. - Form inputs: minimum height
44px. - Spacing between touch targets: minimum
8px.
1.6 Accessibility Requirements#
1.6.1 WCAG 2.1 AA Compliance#
- Color contrast: All text meets 4.5:1 against background (AA). Large text (18px+ bold or 24px+) meets 3:1.
- Focus indicators: All interactive elements have visible focus ring (
shadow-focus, ember-orange, 3px offset). No focus trap except in modals. - Keyboard navigation: All functionality available via keyboard. Tab order follows visual order. Skip link provided for main content.
- Screen reader: All icons have
aria-label. Images havealttext. Forms havelabelassociations. Live regions for toast notifications (aria-live="polite"). - Reduced motion: Respect
prefers-reduced-motion: reduce— disable animations, transitions, auto-playing content.
1.6.2 ARIA Patterns#
- Modal:
role="dialog",aria-modal="true",aria-labelledby(title ID), focus trap. - Toast:
role="status",aria-live="polite",aria-atomic="true". - Sidebar:
role="navigation",aria-label="Main navigation". Current page:aria-current="page". - Table:
role="table", headersscope="col", row selectionaria-selected. - Tabs:
role="tablist",role="tab",role="tabpanel",aria-selected,aria-controls. - Accordion:
role="region",aria-expanded,aria-controls. - Loading:
role="status",aria-busy="true",aria-label="Loading".
1.6.3 Form Accessibility#
- All inputs have associated
<label>(not justplaceholder). - Error messages linked via
aria-describedby. - Required fields marked with
aria-required="true"and visual asterisk (colorerror-red). - Grouped fields (radio, checkbox) use
<fieldset>+<legend>. - Date inputs: use
type="date"witharia-labelfor format hint.
1.7 Representative Widget: KPI Card (Depth Validation)#
This widget demonstrates the exact depth applied to every component in this specification.
1.7.1 KPI Card — Specification#
┌─────────────────────────────────────────┐
│ Total Views ↑ 12.3% [sparkline] │ ← Header row
│ 24,582 │ ← Value (heading-lg, 40px)
│ vs last month: 21,890 │ ← Comparison (caption)
└─────────────────────────────────────────┘
1. Data to Show:
| Field | Type | Format | Source |
|---|---|---|---|
label |
string | "Total Views", "Total Clicks", etc. | Static (configurable per card) |
currentValue |
number | Integer with comma separator | report.getDashboard → views.total |
previousValue |
number | Integer with comma separator | report.getDashboard → views.previous |
changePercent |
number | Signed percentage, 1 decimal | Calculated: ((current - previous) / previous) × 100 |
changeDirection |
enum | "up" | "down" | Derived from changePercent sign |
trendData |
number[] | Array of 7 daily values | report.getDashboard → views.trend |
status |
enum | "good" | "neutral" | "bad" | Derived from thresholds (configurable) |
2. Display Pattern:
- Component type:
MetricCard(composite: container + header + body + footer). - Layout: Vertical stack, flex column,
space-3(12px) gaps. - Container:
card-white,radius-lg,shadow-subtle, padding20px. - Header: Label (
caption, 12px,text-tertiary, uppercase) left-aligned. Trend badge right-aligned. - Body: Value (
heading-lg, 40px, weight 500,text-primary). - Footer: Comparison text (
caption, 12px,text-tertiary). Sparkline (inline SVG, 60px wide, 30px tall). - Trend badge: If
changeDirection === "up"→ arrow-up icon +%value, colorforest-teal. If down →error-red. If neutral (within ±1%) →slate. - Sparkline: Line chart (SVG path), stroke color matches trend badge, fill
rgba10% opacity, no axes, no labels, smooth curve. - Status border: If
status === "bad", left bordererror-red3px. If "good", no extra border.
3. Backend Endpoint:
- Primary:
report.getDashboard(tRPC query). - Zod input schema:
z.object({ period: z.enum(["7d", "30d", "90d"]).default("30d") }). - Zod output schema:
z.object({ views: z.object({ total: z.number(), previous: z.number(), trend: z.array(z.number()) }) }). - Auth: Requires
CLIENT|EDITOR|VIEWERrole. Returns403for unauthenticated.
4. Fetch Frequency:
- Initial load: On dashboard mount.
- Polling: Every 60 seconds (
staleTime: 60s,refetchInterval: 60s). - Background refresh: On window focus (if
staleTimeexceeded). - Manual refresh: "Refresh All" button triggers
utils.invalidate()for all dashboard queries.
5. Data Manipulations:
changePercent:((current - previous) / previous * 100).toFixed(1).changeDirection:changePercent > 0 ? "up" : changePercent < 0 ? "down" : "neutral".trendData: Raw array from API. No manipulation (client renders as-is).status: Configurable thresholds. Default:changePercent >= 5%→ "good",changePercent <= -5%→ "bad", else "neutral".- Formatting:
currentValue.toLocaleString("en-IN")for Indian number format (comma separators).
6. Why Structured This Way:
- 60s polling: Views/clicks are high-velocity metrics. Real-time not needed (no live dashboard), but stale data hurts trust. 60s balances freshness and server load.
- Sparkline not full chart: Saves space in card grid. Full chart available in dedicated chart section below. Sparkline gives trend context without detail.
- Indian number format: Target market is India.
en-INlocale uses lakhs/crores (e.g., 1,00,000). - Color-coded trend: Immediate visual signal. Green = good, red = bad. No cognitive load.
- Left border for bad status: Draws attention without overwhelming the card. Subtle but noticeable.
- No bold weight: Brand constraint. 500 weight on 40px is visually heavy enough.
7. Interaction Flows:
- Hover: Card lifts (
translateY(-1px),shadow-sm), 200ms transition. No click action (read-only card). - Click: None (unless linked to detail view, configurable per card type).
- Context menu: None.
- Loading: Skeleton card (pulsing rectangle for label, larger rectangle for value, thin line for sparkline). Same dimensions as loaded card.
- Error: Card shows inline error state. Icon (alert triangle,
error-red), "Data unavailable", "Retry" button (ghost). Other cards unaffected.
8. Error States:
- Empty: If
currentValue === 0, show "0" as value, sparkline flat line at bottom, no trend badge. Not an error — just no data yet. - Loading: Skeleton (as described above).
- Error: Inline error within card. Other cards show their data.
- Partial: If
trendDatais missing butcurrentValuepresent, show value + "Trend unavailable" text instead of sparkline.
9. Role-Based Variations:
- CLIENT: Full card, all data, no restrictions.
- EDITOR: Same as CLIENT. No differences.
- VIEWER: Same as CLIENT. No differences (read-only metric).
- ADMIN: Not shown in client dashboard. Admin has own KPI dashboard with different metrics.
10. Threshold Configuration:
Per-metric thresholds are configurable via dashboard.config (stored in localStorage or user preferences):
views.warningThreshold: 10% drop (default).views.alertThreshold: 25% drop (default).reviews.warningThreshold: 2-star average (default).reviews.alertThreshold: 1-star average (default).- Thresholds affect
statusderivation and trigger toast notifications when crossed.
1.8 Icon System#
- Library: Lucide React (
lucide-react). - Stroke width: 1.5px (default from tokens).
- Sizes:
sm(16px),md(20px),lg(24px). - Color: Inherits from text color by default. Can be overridden per context.
- Usage: Always paired with text label (except icon-only buttons which have
aria-label). - Animated icons:
Loader2for spinners (rotate animation),Activityfor live indicators (pulse).
1.9 Data Visualization (Charts)#
- Library: Recharts (React).
- Theme: All charts use design tokens for colors.
- Line chart: Series colors from
datatokens (series-1throughseries-5). Grid linesgrid(mist). Axis textcaption(12px,text-tertiary). No bold. - Bar chart: Same colors. Rounded top corners (2px). Bar gap: 8px.
- Pie chart: Colors from
datatokens. Labels outside, connector lines. Legend below. - Area chart: Fill opacity 12%. Stroke width 2px.
- Sparkline: No axes, no labels, no grid. Stroke width 2px. Smooth curve.
- Tooltip:
card-whitebackground,shadow-sm,radius-lg, padding 12px. Header:body-smweight 500. Value:bodyweight 400. - Legend:
captionsize, horizontal layout, clickable to toggle series. - Empty chart: "No data available" centered,
text-tertiary, with icon. - Loading chart: Skeleton rectangle with pulsing animation, same aspect ratio.
1.10 Form Validation Patterns#
- Library: Zod (shared with backend schemas).
- Pattern: React Hook Form + Zod resolver.
- Behavior: Validate on blur (not on every keystroke). Re-validate on submit.
- Error display: Below field,
caption(12px),error-redcolor, error icon left. - Success display: Check icon right of field,
forest-tealcolor (optional, for complex forms). - Required marker: Asterisk after label,
error-redcolor. - Disabled submit: If form invalid or submitting. Button shows spinner.
- Field-level: Zod schema from backend (shared package) ensures client and server validation match.
- Custom validators: Phone (Indian format), URL (with protocol), GSTIN (Indian tax ID), PIN code (6 digits).
1.11 File Upload Patterns#
- Drag & drop: Visual drop zone with dashed border (
mist),radius-lg, icon + text. Hover: borderember-orange, backgroundrgba(236,101,43,0.04). - File types: Images (jpg, png, webp, gif), documents (pdf), max 10MB per file.
- Validation: Type check, size check, dimension check (images). Error per file, dismissible.
- Progress: Linear progress bar (3px,
ember-orange), percentage text, file name. - Preview: Thumbnail for images, icon + name for documents. Remove button (X, top-right).
- Upload: Direct to storage (S3/Cloudflare R2) via presigned URL from
media.getUploadUrl(tRPC). Then send URL to backend. - Multiple files: Grid of thumbnails, max 10 files per upload.
1.12 Search & Filter Patterns#
- Search input: Debounced 300ms. Icon left (Search), clear button right (X appears when text present).
- Filter pills:
radius-full,space-1padding, removable (X icon). Stacked horizontally with wrap. - Filter sidebar: Slide-over from right (desktop), bottom sheet (mobile). Grouped by category.
- Sort: Dropdown select or clickable table headers. Active sort: icon + highlight.
- Pagination: Page numbers + prev/next. Page size selector (20/50/100).
- URL sync: Filters, sort, page sync to URL query params for shareability and back-button support.
- Empty search: "No results for 'query'" + suggestion to adjust filters.
1.13 Summary#
This section establishes the foundational design system and global patterns that all subsequent sections inherit. Every screen, widget, and interaction documented in Sections 2–15 must comply with:
- Color rules: Ember orange for CTAs only. No bold weight. Deep ink for text.
- Typography: Suisse Intl, 400/500 weights only, exact sizes from token system.
- Spacing: 4px base unit, exact values from token system.
- Components: Button, input, card, table, modal, toast patterns as documented.
- Auth: Better Auth session, role-based guards, tRPC context.
- Data fetching: React Query with categorized refresh rates.
- Error handling: Skeleton, spinner, error boundary, retry patterns.
- Responsive: 4 breakpoints, mobile-first, sidebar collapses.
- Accessibility: WCAG 2.1 AA, focus rings, ARIA patterns, keyboard navigation.
The KPI Card example (Section 1.7) demonstrates the exact depth: 10-point specification for every widget, with exact data fields, endpoints, frequencies, manipulations, and UX rationale.
Next: Section 2 — Pre-Sales & Discovery Screens (/, /pricing, /contact)