Frontend Specs
RankFlow AI — Frontend Specification (Part 1)
The RankFlow AI frontend is built on a token-driven design system named Quiet Signal. Every pixel, color, spacing value, and motion curve is derived from the canonical docs/brand/t
docs/specs/frontend/part-1.mdOn this page
- Section 1 — Design System & Global Foundations
- 1.1 Overview
- 1.2 Color Tokens
- 1.3 Typography Tokens
- 1.4 Spacing & Layout Tokens
- 1.5 Border, Radius & Shadow Tokens
- 1.6 Motion & Animation Tokens
- 1.7 Iconography
- 1.8 Global Component Library
- 1.9 Authentication & Session Patterns
- 1.10 Responsive Architecture
- 1.11 Accessibility & Compliance
Version: 1.0.0 Date: 2026-06-13 Stack: Next.js 14 + React 18 + TypeScript + Tailwind CSS + tRPC + React Query + Better Auth Scope: Sections 1–4 — Global Foundations, Pre-Sales, Authentication, Onboarding Classification: Internal — Engineering Handoff
Section 1 — Design System & Global Foundations#
1.1 Overview#
The RankFlow AI frontend is built on a token-driven design system named Quiet Signal. Every pixel, color, spacing value, and motion curve is derived from the canonical docs/brand/tokens/design-tokens.json file. The system is engineered for medical professionals in India — low cognitive load, high readability, strict accessibility, and zero decorative noise.
All components are controlled React components with forwardRef support. The entire UI is responsive across three primary breakpoints: 1280px+ desktop, 768px tablet, and 375px mobile. The sidebar collapses to a hamburger menu on mobile and tablet.
1.2 Color Tokens#
The color system is anchored on three identity colors: Deep Ink, Paper White, and Ember Orange. All other colors are supporting accents or semantic mappings. Ember Orange is reserved exclusively for CTAs and conversion outcomes; it is never used for decoration, icons, or backgrounds unrelated to user action.
| Token Name | Hex Value | Usage Rule | Frontend Variable |
|---|---|---|---|
color.brand.paper-white |
#F6F6F8 |
Primary canvas, editorial backgrounds, dashboard base | --color-paper-white |
color.brand.deep-ink |
#011821 |
Primary typography, dark surfaces, sidebar background, Rise signal bars | --color-deep-ink |
color.brand.ember-orange |
#EC652B |
CTAs only. Primary action buttons, focus rings, conversion outcomes, booked states. Never used for decoration or icons. | --color-ember-orange |
color.brand.card-white |
#FFFFFF |
Cards, dialogs, inputs, elevated surfaces | --color-card-white |
color.brand.carbon |
#12161E |
Dense technical surfaces, secondary dark panels | --color-carbon |
color.brand.deep-indigo |
#111A4A |
Charts, supporting dark accents | --color-deep-indigo |
color.brand.midnight-teal |
#023247 |
Technical panels, information foregrounds, dark data accents | --color-midnight-teal |
color.brand.forest-teal |
#167E6C |
Verified success, live state, positive evidence | --color-forest-teal |
color.brand.sky-blue |
#7EA7E9 |
Informational data series, supporting illustrations | --color-sky-blue |
color.brand.pale-cyan |
#C1E8EF |
Information backgrounds, selected states | --color-pale-cyan |
color.brand.mint |
#44B48B |
Secondary positive data, illustrations | --color-mint |
color.brand.lavender |
#9F7AEE |
Secondary data series only | --color-lavender |
color.brand.mist |
#E3E4E8 |
One-pixel borders, rules, separators | --color-mist |
color.brand.fog |
#A9ACB6 |
Disabled controls, non-essential decoration | --color-fog |
color.brand.slate |
#7C7F88 |
Tertiary labels, muted metadata | --color-slate |
color.brand.graphite |
#3B3E47 |
Accessible secondary text, icons | --color-graphite |
color.brand.error-red |
#C2442A |
Destructive actions, error states | --color-error-red |
Semantic Color Mapping Table:
| Semantic Token | Resolved Value | Usage |
|---|---|---|
text-primary |
#011821 |
All body text, headings, labels |
text-secondary |
#3B3E47 |
Descriptions, secondary labels, helper text |
text-tertiary |
#7C7F88 |
Metadata, timestamps, placeholders |
text-disabled |
#A9ACB6 |
Disabled input text, inactive tab labels |
text-inverse |
#F6F6F8 |
Text on dark surfaces (sidebar, dark buttons) |
text-link |
#023247 |
Inline links, navigational anchors |
text-link-hover |
#011821 |
Link hover state |
surface-canvas |
#F6F6F8 |
Page background |
surface-card |
#FFFFFF |
Card and dialog backgrounds |
surface-elevated |
#FFFFFF |
Modals, dropdowns, popovers |
surface-dark |
#011821 |
Sidebar, dark headers |
surface-dark-secondary |
#12161E |
Admin panels, technical dark zones |
border-default |
#E3E4E8 |
Input borders, card borders, table dividers |
border-strong |
#A9ACB6 |
Focused input borders, active tab borders |
action-primary |
#011821 |
Primary button background (non-CTA) |
action-accent |
#EC652B |
CTA button background, focus indicators |
action-accent-foreground |
#011821 |
Text on Ember Orange buttons |
focus |
#EC652B |
Focus ring color, outline color |
success |
#167E6C |
Success toasts, verified badges, live indicators |
success-soft |
#44B48B |
Secondary success backgrounds |
warning |
#EC652B |
Warning toasts, alert badges |
error |
#C2442A |
Error toasts, invalid input borders, destructive buttons |
info |
#023247 |
Info toasts, informational badges |
info-soft |
#C1E8EF |
Info backgrounds, callout boxes |
Data Visualization Colors:
| Series | Hex | Usage |
|---|---|---|
data-series-1 |
#167E6C |
Primary metric (e.g., citation health) |
data-series-2 |
#7EA7E9 |
Secondary metric (e.g., GBP views) |
data-series-3 |
#9F7AEE |
Tertiary metric (e.g., social engagement) |
data-series-4 |
#44B48B |
Quaternary metric |
data-series-5 |
#023247 |
Quinary metric |
data-attention |
#EC652B |
Threshold breaches, anomaly markers |
data-grid |
#E3E4E8 |
Chart grid lines, table dividers |
Gradient Prohibition: No blue-purple gradients are permitted anywhere in the UI. The only permitted gradients are subtle rgba(1, 24, 33, 0.02) to transparent overlays for image masks on marketing pages.
1.3 Typography Tokens#
Font family is Suisse Intl with a strict hierarchy. No bold weight (700) is permitted anywhere in the UI. The maximum weight used is 600 (semibold). Fallback stack ensures cross-platform rendering.
| Token | Value | Line Height | Letter Spacing | Usage |
|---|---|---|---|---|
font-size-caption |
12px |
18px |
0.01em |
Badges, timestamps, table footers |
font-size-body-sm |
14px |
21px |
0 |
Form labels, helper text, secondary body |
font-size-body |
16px |
24px |
0 |
Primary body text, input values |
font-size-body-lg |
18px |
26px |
-0.01em |
Lead paragraphs, large descriptions |
font-size-subheading |
20px |
28px |
-0.01em |
Card titles, section subheadings |
font-size-heading-sm |
24px |
32px |
-0.02em |
Page titles in dashboard |
font-size-heading |
28px |
36px |
-0.02em |
Hero titles, onboarding step titles |
font-size-heading-lg |
40px |
44px |
-0.02em |
Landing page hero H1 |
font-size-display |
60px |
66px |
-0.03em |
Marketing homepage hero (desktop only) |
| Weight Token | Value | Allowed Context |
|---|---|---|
font-weight-light |
300 |
Large display headings, marketing hero |
font-weight-regular |
400 |
Body text, descriptions, inputs |
font-weight-medium |
500 |
Buttons, navigation labels, tab labels |
font-weight-semibold |
600 |
Card titles, data values, active states |
Font Stack Declaration:
--font-primary: 'Suisse Intl', 'SuisseIntl', 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Suisse Intl Mono', 'SuisseIntlMono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace;
1.4 Spacing & Layout Tokens#
Base unit is 4px. All spacing values are multiples of 4. This ensures perfect alignment and eliminates half-pixel anti-aliasing artifacts on non-retina displays.
| Token | Value | Usage |
|---|---|---|
space-0 |
0 |
Zero spacing |
space-1 |
4px |
Tight gaps, icon padding |
space-2 |
8px |
Inline element gaps, tight button padding |
space-3 |
12px |
Standard element gaps, icon + text pairs |
space-4 |
16px |
Card internal padding (small), form field gaps |
space-5 |
20px |
Medium component gaps |
space-6 |
24px |
Standard card padding, modal padding |
space-8 |
32px |
Section internal gaps, large card padding |
space-10 |
40px |
Page section vertical gaps |
space-12 |
48px |
Large component separations |
space-16 |
64px |
Marketing section gaps |
space-18 |
72px |
Hero section padding |
space-20 |
80px |
Major page sections |
space-24 |
96px |
Full-page vertical rhythm |
space-25 |
100px |
Marketing page maximum gaps |
space-36 |
144px |
Extreme spacing (rare) |
Layout Tokens:
| Token | Value | Usage |
|---|---|---|
page-max-width |
1200px |
Maximum content width for all pages |
section-gap |
80px |
Vertical gap between major page sections |
card-padding |
24px |
Standard padding inside cards |
element-gap |
12px |
Gap between sibling elements in a form or list |
1.5 Border, Radius & Shadow Tokens#
| Token | Value | Usage |
|---|---|---|
border-width-default |
1px |
Standard borders (inputs, cards, tables) |
border-width-strong |
2px |
Active states, focus borders, error states |
border-radius-none |
0 |
Full-bleed images, data tables |
border-radius-sm |
2px |
Small tags, micro-buttons |
border-radius-lg |
8px |
Primary UI radius: buttons, cards, inputs, modals |
border-radius-xl |
12px |
Large cards, hero containers |
border-radius-full |
9999px |
Pills, badges, avatars |
Shadow Tokens:
| 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 resting state, input resting state |
shadow-outline |
0 0 0 1px rgba(87, 90, 100, 0.12) |
Subtle elevation, selected rows |
shadow-inner |
inset 0 0 0 1px rgba(0, 0, 0, 0.05) |
Inset inputs, pressed states |
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) |
Dropdowns, hover-elevated cards |
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 ring for all interactive elements |
1.6 Motion & Animation Tokens#
| Token | Value | Usage |
|---|---|---|
duration-instant |
100ms |
Color transitions, opacity changes |
duration-fast |
150ms |
Button hover states, icon rotations |
duration-base |
200ms |
Card hover lifts, dropdown opens |
duration-slow |
300ms |
Modal enter/exit, page transitions |
| Easing Token | Value | Usage |
|---|---|---|
easing-standard |
cubic-bezier(0.2, 0, 0, 1) |
Standard transitions |
easing-enter |
cubic-bezier(0, 0, 0, 1) |
Elements entering viewport |
easing-exit |
cubic-bezier(0.3, 0, 1, 1) |
Elements leaving viewport |
1.7 Iconography#
- Icon Family: Lucide
- Stroke Width:
1.5px - Sizes:
16px(sm),20px(md),24px(lg) - Color Rule: Icons inherit
text-primaryortext-secondaryviacurrentColor. Only theaction-accenticons (CTA arrows, outcome indicators) useember-orange. - Iconography Prohibition: No custom illustrations, no emoji, no SVG icons outside the Lucide set. All icons must be monochrome.
1.8 Global Component Library#
Every component is a controlled, typed React component with forwardRef support. All components use the design tokens above via CSS custom properties mapped through Tailwind config. No inline styles are permitted.
1.8.1 Button System#
Three variants are permitted: Primary, Accent (CTA), Ghost. No other button styles are allowed. No bold text in buttons. Maximum weight is medium (500).
Primary Button Specification:
| Property | Value | Notes |
|---|---|---|
| Background | action-primary (#011821) |
|
| Foreground | text-inverse (#F6F6F8) |
|
| Border-radius | 8px |
border-radius-lg |
| Padding | 10px 16px |
space-2 vertical, space-4 horizontal |
| Font | body-sm (14px/21px) |
Weight medium (500) |
| Hover | Background midnight-teal (#023247) |
Duration 150ms, easing standard |
| Active | Scale 0.98, shadow-inner applied |
|
| Disabled | Opacity 0.5, cursor not-allowed, no hover transform |
|
| Loading | Spinner icon replaces text, width preserved, aria-busy="true" |
Spinner: Loader2 from Lucide, 16px, text-inverse |
| Focus | shadow-focus ring, offset 0 |
Accent Button (CTA Only) Specification:
| Property | Value | Notes |
|---|---|---|
| Background | action-accent (#EC652B) |
|
| Foreground | action-accent-foreground (#011821) |
|
| Border-radius | 8px |
|
| Padding | 10px 16px |
|
| Font | body-sm (14px/21px) |
Weight medium (500) |
| Hover | Background darkens to #D95A23 |
Duration 150ms |
| Active | Scale 0.98 |
|
| Disabled | Opacity 0.5 |
|
| Loading | Spinner with currentColor #011821 |
|
| Focus | shadow-focus ring |
|
| Constraint | Only one Accent Button per viewport | No decorative use. Never used for secondary actions. |
Ghost Button Specification:
| Property | Value | Notes |
|---|---|---|
| Background | transparent |
|
| Foreground | text-primary (#011821) |
|
| Border | 1px solid border-default (#E3E4E8) |
|
| Border-radius | 8px |
|
| Padding | 10px 16px |
|
| Hover | Background rgba(1, 24, 33, 0.04), border border-strong |
|
| Active | shadow-inner |
|
| Disabled | Opacity 0.5, border border-default |
|
| Focus | shadow-focus ring |
Button Size Matrix:
| Size | Height | Padding | Font Size | Usage |
|---|---|---|---|---|
sm |
32px |
6px 12px |
12px |
Inline actions, table rows |
md |
40px |
10px 16px |
14px |
Standard forms, modals |
lg |
48px |
12px 24px |
16px |
Marketing CTAs, hero actions |
Button Component Interface:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant: "primary" | "accent" | "ghost";
size?: "sm" | "md" | "lg";
loading?: boolean;
children: React.ReactNode;
}
1.8.2 Input System#
Text Input Specification:
| Property | Value | Notes |
|---|---|---|
| Background | surface-card (#FFFFFF) |
|
| Border | 1px solid border-default (#E3E4E8) |
|
| Border-radius | 8px |
|
| Padding | 10px 12px |
|
| Font | body (16px/24px) |
Weight regular (400), color text-primary |
| Placeholder | text-secondary (#3B3E47) |
|
| Hover | Border border-strong (#A9ACB6) |
|
| Focus | Border focus (#EC652B), shadow-focus applied |
|
| Error | Border error (#C2442A), 0 0 0 3px rgba(194, 68, 42, 0.28) |
|
| Disabled | Background surface-canvas, color text-disabled, border border-default |
|
| Read-only | Background surface-canvas, border border-default, no focus ring |
Select Input Specification:
- Same as text input with a
20pxchevron icon (ChevronDownfrom Lucide) on the right,12pxpadding from edge. - Dropdown menu:
shadow-sm, border-radius8px, backgroundsurface-card, max-height240px, overflow-yauto, border1px solidborder-default. - Option hover: Background
rgba(1, 24, 33, 0.04). - Option selected: Background
pale-cyan(#C1E8EF), colortext-primary. - Option font:
body-sm(14px), weightregular. - Empty state:
No optionstext,text-secondary, centered.
Textarea Specification:
- Same as text input, min-height
96px(space-24), resizeverticalonly. - Max-height:
400px. - Line-height:
24px.
Date Input Specification:
- Uses native
<input type="date">with custom styling viaappearance-none. - Calendar icon:
Calendarfrom Lucide,20px, colortext-secondary, positioned right12px. - Value format (tRPC payload):
YYYY-MM-DD(ISO). - Display format:
DD MMM YYYY(e.g.,13 Jun 2026). - Min/max attributes supported via props.
Input Component Interface:
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
hint?: string;
iconLeft?: LucideIcon;
iconRight?: LucideIcon;
}
1.8.3 Card System#
Three card types: Metric, Content, Action.
Metric Card Specification:
| Property | Value | Notes |
|---|---|---|
| Background | surface-card |
|
| Border | 1px solid border-default |
|
| Border-radius | 8px |
|
| Padding | 24px |
|
| Shadow | shadow-subtle |
|
| Layout | Vertical stack, gap 12px |
|
| Header | Label (font-size-caption, text-secondary) + optional info icon (Info, 16px) |
|
| Value | font-size-heading-sm (24px), weight semibold (600), color text-primary |
|
| Delta | font-size-caption, color forest-teal (positive) or error-red (negative), prefixed with ↑ or ↓ |
|
| Hover | shadow-sm, translateY -1px, duration 200ms |
Only on dashboard grid |
Content Card Specification:
| Property | Value | Notes |
|---|---|---|
| Background | surface-card |
|
| Border | 1px solid border-default |
|
| Border-radius | 8px |
|
| Padding | 24px |
|
| Shadow | shadow-subtle |
|
| Header | Title (font-size-subheading, weight medium), optional subtitle (font-size-body-sm, text-secondary) |
|
| Body | font-size-body, text-primary, gap 16px between paragraphs |
|
| Footer | Optional border-top 1px solid border-default, padding-top 16px, aligned right |
Action Card Specification:
- Same as Content Card but with hover state suggesting clickability.
- Hover: Border
border-strong,shadow-sm, cursorpointer, translateY-1px. - Active:
shadow-inner, scale0.995. - Contains a primary action button in the footer or a clickable header with
arrow-righticon. - Must have
role="button"andtabIndex={0}if the entire card is clickable.
Card Component Interface:
interface CardProps {
variant: "metric" | "content" | "action";
title?: string;
subtitle?: string;
children: React.ReactNode;
footer?: React.ReactNode;
onClick?: () => void;
}
1.8.4 Table System#
Base Table Specification:
| Property | Value | Notes |
|---|---|---|
| Wrapper Background | surface-card |
|
| Wrapper Border | 1px solid border-default |
|
| Wrapper Border-radius | 8px |
Applied to wrapper, not cells |
| Header Row Background | surface-canvas |
|
| Header Row Border-bottom | 1px solid border-default |
|
| Header Text | font-size-caption, weight medium (500), color text-secondary, uppercase, letter-spacing 0.05em |
|
| Body Row Background | surface-card |
|
| Body Row Border-bottom | 1px solid border-default |
Last row has no border |
| Body Text | font-size-body-sm, color text-primary |
|
| Row Hover | Background rgba(1, 24, 33, 0.02) |
|
| Row Selected | Background pale-cyan (#C1E8EF) |
|
| Empty State | Centered font-size-body-sm, text-secondary, with SearchX icon, padding 48px |
Sortable Column Specification:
- Header clickable, cursor
pointer. - Sort indicator:
ArrowUp/ArrowDownicon,16px,text-secondary, toggles on click. - Default sort: None; first click ascending, second click descending, third click clears.
- Active sort header: Color
text-primary, icontext-primary. - Multi-sort: Not supported in v1.0; single column sort only.
Pagination Specification:
- Position: Footer of table, right-aligned, padding
16px. - Components: "Previous" / "Next" ghost buttons, page numbers as ghost buttons (
smsize). - Active page: Background
surface-dark, colortext-inverse, border-radius8px. - Page size selector:
20/50/100rows, select input,smsize. - Ellipsis:
...text,text-secondary, non-clickable.
Filters Specification:
- Filter bar above table, horizontal row, gap
12px, padding16px, border-bottom1px solidborder-default. - Each filter:
smsize input or select, ghost button style,min-width: 120px. - Clear filters: Ghost button with
Xicon, appears when any filter is active. - Filter state: Synced to URL query parameters (
?status=ACTIVE&sort=createdAt:desc). - Filter count badge: If filters active, show count in a
fullradius badge, backgroundember-orange, colortext-inverse,font-size-caption.
Table Component Interface:
interface TableProps<T> {
data: T[];
columns: ColumnDef<T>[];
sortable?: boolean;
pagination?: { page: number; pageSize: number; total: number };
filters?: FilterDef[];
emptyState?: React.ReactNode;
onRowClick?: (row: T) => void;
}
1.8.5 Modal System#
Three modal types: Confirmation, Form, Detail.
Modal Wrapper Specification:
| Property | Value | Notes |
|---|---|---|
| Backdrop | rgba(1, 24, 33, 0.60) |
z-index: 400, click to close (unless persistent) |
| Panel Background | surface-card |
|
| Panel Border-radius | 12px (xl) |
|
| Panel Padding | 24px |
|
| Panel Max-width | 560px (default), 720px (wide), 400px (narrow) |
|
| Shadow | shadow-xl |
|
| Entry Animation | Opacity 0 → 1, translateY 8px → 0, duration 300ms, easing enter |
|
| Exit Animation | Reverse of entry | |
| Focus Trap | First focusable element auto-focused; Tab cycles within modal; Escape closes |
|
| Scroll Lock | overflow: hidden on body when open |
Confirmation Modal Specification:
- Icon:
AlertTriangleorCheckCircle,24px, colorwarningorsuccess, centered above title. - Title:
font-size-heading-sm, weightmedium, colortext-primary, centered. - Description:
font-size-body,text-secondary, centered, max-width400px. - Footer: Two buttons side by side, gap
12px, centered. Left: Ghost "Cancel". Right: Accent "Confirm" (destructive actions useerror-redbackground,text-inverseforeground). - Danger variant: Accent button uses
error-redbackground. Icon isAlertTriangle. - Width:
400px(narrow).
Form Modal Specification:
- Same wrapper, body contains a form with vertical stack gap
16px. - Footer: Ghost "Cancel" + Primary "Save" or Accent "Submit", right-aligned.
- Validation: Inline errors appear below fields, modal does not close on invalid submit.
- Submitting: Footer buttons disabled, primary button shows spinner.
- Width:
560px(default). - Multi-step: Not supported in v1.0.
Detail Modal Specification:
- Wide variant (
720px). - Body: Scrollable if content exceeds
70vh, custom scrollbar (8pxthumb,midnight-teal). - Header: Title (
font-size-heading-sm) + closeXicon (X,20px), right-aligned. - Footer: Optional action buttons or "Close" ghost button.
- No centering; left-aligned text.
Modal Component Interface:
interface ModalProps {
type: "confirmation" | "form" | "detail";
open: boolean;
onClose: () => void;
title: string;
description?: string;
children?: React.ReactNode;
footer?: React.ReactNode;
size?: "narrow" | "default" | "wide";
persistent?: boolean;
}
1.8.6 Toast System#
Toast container: Fixed top-right, z-index: 800, gap 12px between toasts, max-width 400px, padding 16px from viewport edge.
Toast Variants:
| Variant | Background | Border | Icon | Text Color | Duration |
|---|---|---|---|---|---|
| Success | forest-teal (#167E6C) |
1px solid rgba(255,255,255,0.1) |
CheckCircle |
text-inverse |
5000ms |
| Error | error-red (#C2442A) |
1px solid rgba(255,255,255,0.1) |
XCircle |
text-inverse |
8000ms |
| Warning | ember-orange (#EC652B) |
1px solid rgba(255,255,255,0.1) |
AlertTriangle |
text-inverse |
6000ms |
| Info | midnight-teal (#023247) |
1px solid rgba(255,255,255,0.1) |
Info |
text-inverse |
5000ms |
Toast Behavior:
- Entry: Slide from right
translateX(100%) → 0, opacity0 → 1, duration300ms, easingenter. - Exit: Slide right, opacity
1 → 0, duration200ms, easingexit. - Progress bar: Thin line (
2px) at bottom, colorrgba(255,255,255,0.3), animates from full width to0over duration. - Hover: Pauses progress bar, resume on mouse leave.
- Dismiss: Click
Xicon (16px, top-right) or swipe right on mobile (>50pxswipe). - Max stack:
5toasts; overflow pushes oldest out with exit animation. - Accessibility:
role="status"(non-critical) orrole="alert"(critical),aria-live="polite"orassertive.
Toast Component Interface:
interface ToastOptions {
id: string;
variant: "success" | "error" | "warning" | "info";
title: string;
description?: string;
duration?: number;
action?: { label: string; onClick: () => void };
}
1.8.7 Badge & Pill System#
| Variant | Background | Foreground | Border-radius | Padding | Usage |
|---|---|---|---|---|---|
default |
surface-canvas |
text-secondary |
9999px |
4px 12px |
Neutral status |
success |
forest-teal |
text-inverse |
9999px |
4px 12px |
Live, verified, active |
warning |
ember-orange |
text-inverse |
9999px |
4px 12px |
Pending, trial, warning |
error |
error-red |
text-inverse |
9999px |
4px 12px |
Failed, cancelled, expired |
info |
pale-cyan |
text-primary |
9999px |
4px 12px |
Informational, draft |
outline |
transparent |
text-secondary |
9999px |
4px 12px |
Tags, categories |
Font: font-size-caption (12px), weight medium (500).
1.8.8 Sidebar Component#
| Property | Value | Notes |
|---|---|---|
| Background | surface-dark (#011821) |
|
| Width | 240px |
width: 240px |
| Width Collapsed | 64px |
Icon-only mode on large desktop (optional toggle) |
| Foreground | text-inverse (#F6F6F8) |
|
| Foreground Muted | fog (#A9ACB6) |
Inactive items |
| Item Hover | rgba(246, 246, 248, 0.08) |
|
| Item Active | rgba(246, 246, 248, 0.12) |
|
| Active Indicator | ember-orange (#EC652B) |
2px left border on active item |
| Font | body-sm (14px) |
Weight medium (500) |
| Icon Size | 20px |
Color inherits muted or inverse |
| Gap | 12px |
Between icon and label |
| Section Gap | 24px |
Between nav groups |
| Logo Area | Height 64px, padding 16px 24px |
Logo 160px wide max |
Mobile Sidebar Behavior:
mdand below: Sidebar becomes full-screen overlay,100vw,100vh, backgroundsurface-dark, slides in from left withduration-slow(300ms),easing-enter.- Backdrop:
rgba(1, 24, 33, 0.60),z-index: 399, tap to close. - Close trigger: Tap backdrop, swipe left (
>50px), or tapXicon (24px, top-left). - Header: Logo + close button, padding
16px 24px. - No collapsed icon-only mode on mobile.
Sidebar Navigation Items:
| Route | Label | Icon | Roles | Badge Logic |
|---|---|---|---|---|
/dashboard |
Overview | LayoutDashboard |
ADMIN, CLIENT, EDITOR, VIEWER | None |
/dashboard/gbp |
Google Business | MapPin |
ADMIN, CLIENT, EDITOR, VIEWER | Pending review count |
/dashboard/social |
Social Media | Share2 |
ADMIN, CLIENT, EDITOR | Pending posts |
/dashboard/citations |
Citations | Link |
ADMIN, CLIENT, EDITOR, VIEWER | NAP mismatch count |
/dashboard/content |
Content | FileText |
ADMIN, CLIENT, EDITOR | Pending approval count |
/dashboard/site |
Website | Globe |
ADMIN, CLIENT, EDITOR, VIEWER | None |
/dashboard/reports |
Reports | BarChart3 |
ADMIN, CLIENT, EDITOR, VIEWER | New report indicator |
/dashboard/leads |
Leads | Inbox |
ADMIN, CLIENT, EDITOR | New leads count |
/dashboard/settings |
Settings | Settings |
ADMIN, CLIENT | None |
/dashboard/billing |
Billing | CreditCard |
ADMIN, CLIENT | Past due indicator |
/admin |
Admin | Shield |
ADMIN | Failed jobs count |
1.9 Authentication & Session Patterns#
1.9.1 Better Auth Session Context#
The application wraps all pages in a SessionProvider from better-auth/react. Session data is fetched server-side on initial render via getServerSession in createTRPCContext. Client-side, the session is available via the useSession() hook.
Session Shape:
{
"user": {
"id": "usr_abc123",
"email": "dr.smith@example.com",
"name": "Dr. Smith",
"image": "https://cdn.rankflow.ai/avatars/usr_abc123.png",
"role": "CLIENT"
},
"session": {
"id": "sess_xyz789",
"expiresAt": "2026-06-20T06:06:20Z",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
}
}
- Session refresh: Automatic via Better Auth
updateAge: 60 * 60 * 24(every 24 hours). - Idle timeout: No explicit idle timeout; session expires after 7 days (
expiresIn: 60 * 60 * 24 * 7). - Multi-session: Supported via
auth.listSessionsandauth.revokeSession. UI shows active sessions in/dashboard/settings/security. - Session persistence:
Remember mecheckbox extends session to 30 days on the device; stored vialocalStorageflagrankflow_remember_me.
1.9.2 Role Checks#
Role is stored in User.role enum: ADMIN, CLIENT, EDITOR, VIEWER. Frontend role enforcement is defensive: every route checks session.user.role before rendering restricted UI.
Role-Based Component Wrapper:
interface WithRoleProps {
allowedRoles: ("ADMIN" | "CLIENT" | "EDITOR" | "VIEWER")[];
children: React.ReactNode;
fallback?: React.ReactNode;
}
Redirect Rules:
| Scenario | Source | Target | Toast |
|---|---|---|---|
Unauthenticated user accessing /dashboard/* |
Any /dashboard route |
/login?redirect=/dashboard |
None |
VIEWER accessing /dashboard/settings |
/dashboard/settings |
/dashboard |
"Settings require editor access" (warning) |
EDITOR accessing /dashboard/billing |
/dashboard/billing |
/dashboard |
"Billing is restricted to the practice owner" (warning) |
CLIENT accessing /admin |
/admin |
/dashboard |
"Admin access required" (error) |
ADMIN accessing /dashboard |
/dashboard |
/admin |
None (optional redirect) |
1.9.3 tRPC Client Setup#
The tRPC client is initialized in src/lib/trpc.ts with httpBatchLink and superjson transformer.
Client Configuration:
export const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: "/api/trpc",
headers() {
return {
"x-request-id": crypto.randomUUID(),
};
},
}),
],
transformer: superjson,
});
Error Handling Mapping:
| tRPC Code | HTTP | Frontend Behavior | Toast Message |
|---|---|---|---|
BAD_REQUEST |
400 |
Show inline Zod errors; toast with "Please check your input" | Inline + warning toast |
UNAUTHORIZED |
401 |
Redirect to /login; clear local query cache |
None (redirect) |
FORBIDDEN |
403 |
Redirect to /dashboard; toast permission denial |
Error toast |
NOT_FOUND |
404 |
Show empty state with "Not found" message | Info toast |
CONFLICT |
409 |
Show inline error (e.g., duplicate email) | Warning toast |
TOO_MANY_REQUESTS |
429 |
Toast "Too many requests. Please wait 60 seconds." | Warning toast |
INTERNAL_SERVER_ERROR |
500 |
Toast "Something went wrong. Please try again."; log to Sentry | Error toast |
Zod Error Display:
// Example Zod error from tRPC
{
"message": "Invalid input",
"code": "BAD_REQUEST",
"zodError": {
"fieldErrors": {
"email": ["Invalid email address"],
"phone": ["Must be at least 10 characters"]
},
"formErrors": []
}
}
- Field errors map directly to input
errorprops. - Form errors (non-field) display in a toast or an alert banner at the top of the form.
1.9.4 React Query Defaults#
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 60 seconds
cacheTime: 5 * 60 * 1000, // 5 minutes
retry: 1, // one retry for network errors only
refetchOnWindowFocus: true,
refetchOnReconnect: true,
suspense: false,
},
mutations: {
retry: 0,
onError: (error) => {
// Global mutation error handler
showToast({ variant: "error", title: "Action failed", description: error.message });
},
},
},
});
- Dashboard data:
staleTime: 60sensures freshness without excessive refetching. - Auth pages:
refetchOnWindowFocus: falseto prevent session polling on login pages. - Mutations: No retry; immediate feedback via toast.
- Cache invalidation: After
practice.update,location.create,billing.createSubscription, invalidate related queries withutils.practice.get.invalidate().
1.9.5 Error Boundaries#
Every major route segment has a ReactErrorBoundary. The fallback UI is a controlled, non-panicking state that preserves navigation.
Error Boundary Fallback UI:
- Layout: Centered card,
max-width: 400px, padding24px, border-radius8px, backgroundsurface-card, shadowshadow-sm. - Icon:
XCircle,32px, colorerror-red, centered. - Title:
font-size-heading-sm, weightmedium, "Something went wrong", centered, colortext-primary. - Description:
font-size-body-sm,text-secondary, "We've logged this error and are working on a fix.", centered. - Action: Ghost button "Reload page",
mdsize, centered,onClick={() => window.location.reload()}. - Secondary action: Link "Go to dashboard",
text-link, centered.
Logging: Errors are sent to Sentry with x-request-id, user ID, route path, and component stack trace.
1.10 Responsive Architecture#
Breakpoints are derived from design-tokens.json:
| Breakpoint | Width | Layout Behavior |
|---|---|---|
sm |
640px |
Minor adjustments; single column |
md |
768px |
Tablet layout; sidebar collapses to hamburger; 2-column grid becomes 1-column |
lg |
1024px |
Small desktop; sidebar visible, content max-width 960px |
xl |
1280px |
Standard desktop; sidebar 240px, content max-width 1200px |
2xl |
1536px |
Large desktop; content max-width 1200px centered |
Sidebar Behavior:
xland above: Fixed left sidebar,240pxwidth,z-index: 200, always visible,position: fixed.lgand below: Sidebar collapses to a hamburger menu (Menuicon,24px) in the top-left of the header. Header height64px.- Mobile (
mdand below): Sidebar becomes a full-screen overlay,100vw,100vh, backgroundsurface-dark, slides in from left withduration-slow(300ms),easing-enter. - Mobile overlay close: Tap backdrop, swipe left (
>50px), or tapXicon (X,24px, top-left,text-inverse). - Body scroll: Locked when overlay is open (
overflow: hiddenonhtml).
Content Grid:
- Default: Single column with
max-width: 1200px, centered, padding0 24px. - Dashboard: Two-column layout on
xl(sidebar240px+ main contentfill), single column onlgand below. - Marketing pages: Full-width sections with internal
max-width: 1200pxcontainers, padding0 24px. - Onboarding wizard: Centered card,
max-width: 720px, padding0 24px.
Touch Targets:
- All interactive elements must be minimum
44px × 44pxon touch devices. - Button heights on mobile:
48pxminimum. - Input heights on mobile:
48pxminimum. - Table rows on mobile:
56pxminimum height.
1.11 Accessibility & Compliance#
1.11.1 WCAG 2.1 AA Compliance#
- All text meets minimum contrast ratio
4.5:1against its background. - Large text (
18px+or24px+headings) meets3:1. ember-orangeonpaper-whitecontrast ratio is4.6:1(passes AA).deep-inkonpaper-whitecontrast ratio is16.2:1(passes AAA).graphiteonpaper-whitecontrast ratio is7.8:1(passes AA).slateonpaper-whitecontrast ratio is4.5:1(passes AA).fogonpaper-whitecontrast ratio is3.0:1(fails AA; only used for disabled states, which are exempt from contrast requirements under WCAG).
1.11.2 Focus Management#
- Every interactive element has a visible focus ring:
shadow-focus(0 0 0 3px rgba(236, 101, 43, 0.28)). - Focus ring is removed for mouse users via
:focus-visiblepolyfill. - Modal focus trap:
react-focus-lockor equivalent. Focus returns to trigger element on close. - Skip link: First focusable element on every page is a "Skip to content" link, visually hidden until focused (
position: absolute,top: -40px,left: 0,background: deep-ink,color: paper-white,padding: 8px 16px,z-index: 999). - Focus order: Logical DOM order; no positive
tabindexvalues.
1.11.3 ARIA Labels & Roles#
- All icon-only buttons have
aria-labeldescribing the action (e.g.,aria-label="Close modal"). - Navigation menus use
role="navigation",aria-label="Main navigation". - Tables use
role="table", headersrole="columnheader", rowsrole="row", cellsrole="cell". - Modals use
role="dialog",aria-modal="true",aria-labelledbypointing to title element ID. - Toasts use
role="status"(non-critical) orrole="alert"(critical),aria-live="polite"orassertive. - Loading states:
aria-busy="true"on forms and buttons during submission. - Error messages:
aria-describedbyon inputs pointing to error message element ID. - Current page:
aria-current="page"on active sidebar nav item. - Breadcrumbs:
aria-label="Breadcrumb",navelement.
1.11.4 Reduced Motion Support#
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0ms !important;
scroll-behavior: auto !important;
}
}
- The
prefers-reduced-motionflag is read viawindow.matchMediaand stored in a React context (MotionContext). - Components consuming
MotionContextdisable auto-scrolling carousels, auto-advancing wizards, and parallax effects. - Modal entry: Instant opacity change, no translate.
- Toast entry: Instant, no slide.
- Page transitions: None.
- Button hover: Instant color change.
1.11.5 DPDPA 2023 Compliance (Frontend)#
- Consent checkboxes are never pre-checked (opt-in by default).
- Consent withdrawal link is present in footer of every authenticated page: "Manage data consent" →
/dashboard/settings/privacy. - Data export request button is visible in
/dashboard/settingsfor all roles. Triggerscompliance/data-exportInngest event. - All forms that collect personal data display a "Required" or "Optional" label explicitly.
- No third-party cookies are loaded before consent is granted (Google Analytics, Meta Pixel, etc.).
- Cookie banner: Appears on first visit, minimal design, ghost buttons,
ember-orangeprimary action "Accept all".
1.11.6 Screen Reader Support#
- All images have descriptive
alttext. Decorative images usealt="". - Charts have
aria-labelwith summary text (e.g., "Citation health chart showing 85% consistency"). - Form validation errors are announced via
aria-live="polite"region. - Route changes are announced via
aria-live="polite"region with page title. - Loading spinners have
role="progressbar",aria-valuemin="0",aria-valuemax="100",aria-valuenow="indeterminate".