This document is the contract between the Stitch designs and the React code Claude Code will write. Read after AGENTS.md.
The Stitch designs ship as reference HTML files in stitch/batch_1/ and stitch/batch_2/. Treat them as gospel for layout, spacing, and component shape. Treat this document as gospel for tokens, fonts, and the React conversion rules.
The two Stitch batches ship with slightly different color palettes. We pin Batch 1 (the original three screens — automotive_editorial_concierge/DESIGN.md) as the canonical palette. Batch 2 must be brought into alignment.
Why: Batch 1's primary blue (#004f8e) and its softer surface tones match the existing tata-voice-demo.html in the repo, and it's the warmer, more editorial palette. Batch 2 leans cooler and slightly darker. Pinning Batch 1 keeps the system warm.
Tailwind theme is the single source of truth. Component code references text-on-surface, bg-surface-container, etc. — never raw hex codes.
// frontend/tailwind.config.ts
colors: {
// Surfaces
surface: '#f8f9ff', // Magnolia — page background
'surface-dim': '#cbdbf6',
'surface-bright': '#f8f9ff',
'surface-container-lowest': '#ffffff',
'surface-container-low': '#eff3ff',
'surface-container': '#e6eeff',
'surface-container-high': '#dde9ff',
'surface-container-highest': '#d4e3ff',
'surface-variant': '#d4e3ff',
background: '#f8f9ff',
// Text on surfaces
'on-surface': '#0c1c30',
'on-surface-variant': '#414751',
'on-background': '#0c1c30',
'inverse-surface': '#223146',
'inverse-on-surface': '#ebf1ff',
// Outlines
outline: '#717782',
'outline-variant': '#c1c7d3',
// Primary (Tata Denim)
primary: '#004f8e',
'on-primary': '#ffffff',
'primary-container': '#1467b3',
'on-primary-container': '#d6e5ff',
'inverse-primary': '#a3c9ff',
'primary-fixed': '#d3e3ff',
'primary-fixed-dim': '#a3c9ff',
'on-primary-fixed': '#001c39',
'on-primary-fixed-variant': '#004883',
// Secondary
secondary: '#2d628f',
'on-secondary': '#ffffff',
'secondary-container': '#9acbfe',
'on-secondary-container': '#1e5682',
// Tertiary (Tata Red — accents and emergencies only)
tertiary: '#a00020',
'on-tertiary': '#ffffff',
'tertiary-container': '#cd002c',
'on-tertiary-container': '#ffdcdb',
'tertiary-fixed': '#ffdad8',
'tertiary-fixed-dim': '#ffb3b1',
// Error
error: '#ba1a1a',
'on-error': '#ffffff',
'error-container': '#ffdad6',
'on-error-container': '#93000a',
}Rules for tertiary (Tata red). Reserved strictly for:
- The 2px vertical brand stripe in TopBar
- Roadside emergency UI (border, icon, helpline button)
- Provenance pill underline (not background)
- Critical error states (admin only)
Never used for primary buttons, page backgrounds, or large content fills.
fontFamily: {
display: ['Manrope', 'system-ui', 'sans-serif'], // 800 weight for display/headlines
body: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'], // 400/500/700 for body/UI
mono: ['"Geist Mono"', 'ui-monospace', 'monospace'], // 500 for technical data
},
fontSize: {
'display-lg': ['48px', { lineHeight: '1.1', letterSpacing: '-0.02em', fontWeight: 800 }],
'display-md': ['36px', { lineHeight: '1.2', letterSpacing: '-0.02em', fontWeight: 800 }],
'headline-lg': ['28px', { lineHeight: '1.3', letterSpacing: '-0.02em', fontWeight: 800 }],
'headline-sm': ['20px', { lineHeight: '1.4', letterSpacing: '-0.01em', fontWeight: 700 }],
'body-lg': ['18px', { lineHeight: '1.6', fontWeight: 500 }],
'body-md': ['16px', { lineHeight: '1.5', fontWeight: 400 }],
'body-sm': ['14px', { lineHeight: '1.5', fontWeight: 400 }],
'label-mono': ['12px', { lineHeight: '1', letterSpacing: '0.02em', fontWeight: 500 }],
// Mobile override
'display-lg-m': ['32px', { lineHeight: '1.2', letterSpacing: '-0.02em', fontWeight: 800 }],
},Use rules:
- Manrope 800 — page titles, hero headlines, section headers, big prices in cards
- Plus Jakarta Sans 400 — body copy, transcript bubbles, paragraph text
- Plus Jakarta Sans 500-700 — UI labels, button text, captions
- Geist Mono 500 — prices, VINs, dates, IDs, latency numbers, status pills, audit timestamps
spacing: {
// 4-based scale plus design-system tokens
'unit-xs': '4px',
'unit-sm': '8px',
'unit-md': '16px',
'unit-lg': '24px',
'unit-xl': '48px',
},
maxWidth: {
container: '1280px',
},
borderRadius: {
sm: '0.125rem', // 2px
DEFAULT: '0.25rem', // 4px — buttons, inputs
md: '0.375rem', // 6px
lg: '0.5rem', // 8px — cards
xl: '0.75rem', // 12px
full: '9999px', // VoiceButton, pills
},- Outer page gutter: 24px desktop, 16px mobile
- Inter-section vertical spacing: 48px (
unit-xl) - Card padding: 24px desktop, 16px mobile
- Inter-bubble vertical spacing in transcript: 8px
No drop shadows. Use tonal layering and 1px outlines:
- Level 0 (page background):
surface(#f8f9ff) - Level 1 (cards, bubbles):
surface-container-lowest(#ffffff) with1px solid outline-variant(#c1c7d3) - Floating elements only (VoiceButton): one allowed soft shadow:
0 4px 20px rgba(15, 31, 51, 0.05) - Hairline dividers within cards: 1px
outline-variant
A small set of primitives. Build these first; pages compose them.
| Component | Spec |
|---|---|
<TopBar /> |
Fixed 64px height. Background surface. Logo wordmark in Manrope 800 primary, followed by 2px tertiary vertical stripe, then page title in Manrope 800 primary-container. Right side: optional notification/profile icons, or a customer name + entity badge. |
<BrandStripe /> |
The reusable 2px tertiary vertical stripe used between logo and title. Width 2px, height 24px, color tertiary. |
<EntityBadge entity="TMPV" /> |
Small filled pill in primary-container background, on-primary-container text, Geist Mono 12px. Values: TMPV (blue), TML (amber), JLR (rose), PROSPECT (gray). |
<ProvenancePill /> |
"Concept · AI-generated" pill. Background surface-container, border outline-variant, Geist Mono 12px, with a small spark icon. Used on any AI-generated visual. |
| Component | Spec |
|---|---|
<VoiceButton state="idle|listening|processing" /> |
64px circle, primary background, white mic icon. Listening: primary-container background + pulsating outer ring in primary-fixed-dim. Processing: rotating border segment in primary-fixed-dim. Floating bottom-right (24px from edges). |
<ChatInput placeholder onSubmit onAttach /> |
Pill-shaped input, surface-container-low background, paperclip + send icons. |
<TranscriptBubble role="concierge|user" streaming={bool}>{text}</TranscriptBubble> |
Concierge: surface-container-low bg, radius 10px 10px 10px 2px. User: surface-container bg, radius 10px 10px 2px 10px. No tail/balloon. Streaming variant shows a small caret ▍. |
<TypingIndicator /> |
3-dot animation, primary color, used while waiting for sub-agent. |
<BottomUtilityRail /> |
64px tall, holds 4 icons: microphone, info, library, history (Material Symbols Outlined). Active item filled primary bg. |
These render the VisualPayload objects from the Synthesizer. One component per type.
type |
Component | Maps to Stitch screen |
|---|---|---|
model_card |
<ModelCard /> |
S2 (active conversation) — left panel |
model_comparison |
<ModelComparison /> |
derived from S2 components |
emi_panel |
<EmiPanel /> |
S2 — right panel pricing block / S6 Finance left card |
insurance_panel |
<InsurancePanel /> |
S6 Finance middle card |
exchange_panel |
<ExchangePanel /> |
S6 Finance right card |
subscription_quote |
<SubscriptionQuote /> |
S4 Subscription |
trip_map |
<TripMap /> |
S6 EV Advisor |
service_status |
<ServiceStatus /> |
S1 Service |
document_gallery |
<DocumentGallery /> |
S2 Documents |
rsa_dispatch |
<RsaDispatch /> |
S3 Roadside |
generated_image |
<GeneratedImageGallery /> |
S3 Family upload result |
| Component | Spec |
|---|---|
<Card /> |
Level 1 wrapper: surface-container-lowest bg, 1px outline-variant border, 8px radius, 24px desktop / 16px mobile padding. |
<EditorialCard image headline body> |
Card with image filling top 60%. Headline headline-sm, body body-sm on-surface-variant. Used in document gallery, model recommendations. |
<DataRow label value mono?> |
Used inside cards for label-value pairs. Label label-mono on-surface-variant, value body-md on-surface (mono if numeric). |
<AlertCard severity="emergency"> |
Used in Roadside. 1px tertiary-container border, tertiary icon, headline in tertiary headline-lg. |
| Component | Spec |
|---|---|
<Pill selected /> |
Variant/tenure/color pill. Unselected: 1px outline-variant border, transparent bg. Selected: primary-container bg, on-primary-container text. 4px radius (per shape system "soft"). |
<ColorSwatch /> |
28px circle with 2px ring when selected (ring color primary). |
<Slider /> |
Horizontal track 4px tall, surface-container-high fill, primary fill on the active portion, 16px handle in primary. |
<Button variant="filled|outlined|text" tone="primary|tertiary"> |
Filled: primary bg, white text. Outlined: 1px outline-variant, primary text. Text: primary text, no bg/border. Tertiary tone reserved for emergency CTAs. 4px radius. |
- Full-bleed hero with a Tata vehicle image and "Engineered to evolve." overlay
- Single CTA pill: "How can we help you find your Tata today?"
- Below the fold: a single concierge welcome bubble
- Bottom utility rail
- VoiceButton bottom-right
Behavior: clicking the CTA or VoiceButton starts a new session (POST /api/sessions) and navigates to /concierge.
The primary working surface. Split layout, desktop: left panel hosts the current visual payload; right panel hosts the transcript and chat input.
- Left panel default:
<ModelCard />showing the most recently recommended/configured model with 360° spinner controls (drag-to-rotate hint, zoom button) - Below model card:
<VariantPicker />and<ColorSwatchRow /> - Right panel: scrollable transcript of
<TranscriptBubble>components, ending in the streaming agent reply - Below transcript:
<ChatInput>with optional<VoiceButton>adjacent
Behavior: every user turn calls POST /api/sessions/{id}/turns and consumes the SSE stream. As visual.payload events arrive, the left panel swaps to the matching component. As gated.surfaced events arrive, a <GatedActionPrompt /> overlays the right panel with the action summary and a Confirm button.
Shown after generate_visual returns. Three generated variants in a 3-column grid (1-column on mobile), each with the <ProvenancePill> overlay.
Below the grid: 5 action buttons — Regenerate, Try different scene, Save, Share, Email.
Below buttons: success banner ("Your AI visualization has been saved to your showroom portfolio") with View Portfolio CTA.
Behavior: "Regenerate" calls POST /api/visualize/generate with the same prompt. "Try different scene" returns to /concierge with the input focused on the scene picker.
Split layout. Left: <ServiceStatus /> with vehicle title, VIN, status stepper (Received → Inspection → Awaiting Part → Ready), service center info, technician, expected-ready time, and <EstimatedBillCard />. Two buttons: Request Technician Callback (primary filled), View Full Invoice (outlined).
Right: transcript + chat input.
Behavior: "Request Technician Callback" calls the Service sub-agent's request_technician_callback tool (reversible).
Single full-width layout below TopBar.
- Owner summary band: customer name, entity badge, vehicle, VIN
- Horizontal scroll row of 5
<EditorialCard>documents (RC, Insurance, Warranty, Manual, Service History) with VIEW + DOWNLOAD buttons - Below: one concierge bubble with a renewal-reminder note
Centered single-column, max width 720px. Calm, decisive.
- TopBar: page title "Roadside Assistance" with thicker tertiary stripe (vs the standard 2px)
<AlertCard severity="emergency">"Help is on the way."<RsaDispatch />card: Dispatch ID, ETA, Technician, Location, Helpline (all Geist Mono)- Map thumbnail below dispatch card (
<StaticMap />) - Two transcript bubbles: user "tire burst" + concierge "help dispatched"
- Two sticky bottom buttons: Call Technician Now (primary filled), Call Helpline 1800 209 8282 (tertiary outlined)
Behavior: the gated confirmation has already happened before this screen renders. Reaching /roadside means dispatch is confirmed. The Call buttons are tel: links.
Split layout. Left: large vehicle image with <VariantPicker />, <ColorSwatchRow />, "Drag to rotate" hint, 5-cities caption.
Right: <SubscriptionQuote /> card with "Powered by Orix" pill, tenure tabs (18/24/36 months), big monthly price in Manrope 800 primary, breakdown table, footnote, two buttons: Draft Subscription Contract (primary filled, gated), Compare Tenures (outlined). Below right panel: single concierge bubble.
Map-dominant layout. Left 2/3: <StaticMap> with route polyline (Hyderabad → Bengaluru), origin/destination markers (with SOC labels), and charging pins along the route.
Right 1/3: <TripPlanCard> with route title, four data cells (Distance, Battery Health, Start SOC, Est. Time), vertical stops timeline, two buttons (Reserve Kurnool Charger primary, Get SMS Reminders outlined). Above the card: a concierge bubble summarizing the plan.
Three-column desktop grid. Above: large <Headline> "Quotes for Tata Curvv EV · Empowered X 55 · Empowered Oxide · Bangalore" in display-md.
- Card 1:
<EmiPanel>(Loan EMI via Tata Capital) — slider down payment, tenure pills, big EMI in primary, Apply for Loan button - Card 2:
<InsurancePanel>(Insurance via Tata AIG) — IDV row, premium row, add-ons checkboxes, Get Insurance Binder button - Card 3:
<ExchangePanel>(Exchange Your Car) — current vehicle, valuation range, Schedule Inspection button
Below the cards: one concierge bubble synthesizing all three.
Data-dense, no VoiceButton. Replaces VoiceButton with an Export Logs outlined button.
- TopBar: "System Supervisor · Admin" with operator name
- Top row: 4 KPI tiles (Active Conversations, Routing Accuracy, Tool Calls, Pending Confirmations) — values in Manrope 800 display-md
- Section "Live Agent Activity" (left 2/3) —
<LiveActivityFeed />SSE stream of recent events with status pills (SUCCESS green, PROCESSING amber, FAILED red) - Section "Anomalies for Review" (right 1/3) — list of
<AnomalyCard />with Investigate / Dismiss - Section "Audit Log · Last 100 Events" (full width) —
<AuditLogTable />with timestamp / event_type / agent / customer_id_hash / tool / latency_ms, search box, Export Logs button
The most important interaction is /concierge consuming the SSE stream. Implementation contract:
const { send, events, isStreaming } = useTurnStream(sessionId);
// As events arrive, the UI updates incrementally:
events.on('intent.extracted', e => setIntentBadges(e.intents));
events.on('subagent.started', e => setActiveSubAgents(prev => [...prev, e.name]));
events.on('subagent.partial', e => setSubAgentSummary(e.name, e.summary, true));
events.on('subagent.complete', e => setSubAgentSummary(e.name, e.output.summary, false));
events.on('token.delta', e => appendToCurrentBubble(e.text));
events.on('visual.payload', e => setLeftPanel(e));
events.on('gated.surfaced', e => showGatedPrompt(e));
events.on('turn.complete', e => finalizeBubble());Visual feedback rules:
intent.extracted→ small badges briefly appear above the input ("Sales", "Finance", "EV") then fadesubagent.started→ a typing indicator appears for that sub-agenttoken.delta→ tokens stream into the current concierge bubblevisual.payload→ left panel swaps to the matching component with a 200ms cross-fadegated.surfaced→<GatedActionPrompt>slides up from the bottom of the right panel, blocking further input until confirmed or cancelled
Per the Stitch design system: restrained motion, no bouncy animations.
- Bubble entry: 240ms fade + 4px upward slide, ease-out
- Visual payload swap: 200ms cross-fade
- Page transitions: 180ms fade
- VoiceButton pulse: 1.8s ease-in-out infinite (when listening)
- Typing indicator: standard 3-dot pulse 1.2s
No spring animations. No bouncy entrance. No long stagger. The feel is mechanical precision, not toy-like delight.
Below 600px:
- Split layouts collapse to single column, left panel above right
- TopBar reduces logo to icon only
- VoiceButton stays bottom-right but at 56px instead of 64px
- Display headlines drop to mobile sizes (
display-lg-m) - Outer gutter drops to 16px
Above 1024px:
- Full layouts as designed
- Container caps at 1280px max-width
Tablet (600-1024px) inherits desktop layout but with tighter gutters.
- All
<Button>and<Pill>components have visible focus rings (2pxprimaryoutline at 2px offset) - Color contrast meets WCAG AA:
on-surfaceonsurfaceis 14.5:1,on-surface-variantonsurfaceis 8.2:1 - VoiceButton has
aria-label="Start voice input"with state announced viaaria-live - Transcript region has
role="log"witharia-live="polite" - All visual payload components have semantic markup (no div-as-button)
- Reduce-motion users get instant transitions (no fades, no slides)
Use the seeded URL pattern from data/catalog_seed.json:
function scene7Url(imageKey: string, preset: '$BA-1920-925-D$' | '$HC-264-129-D$' | '$HL-50-33-D$' = '$HC-264-129-D$') {
return `https://s7ap1.scene7.com/is/image/tatamotors/${imageKey}?${preset}&fit=crop&fmt=avif-alpha`;
}For the 360° spinner, request 36 frames using the catalog's image_keys_360 array.
Maps come from the backend, not directly from the client. Frontend calls GET /api/maps/static?... and receives a signed URL that's safe to put in an <img> tag.
Material Symbols Outlined, loaded from Google Fonts in the root layout:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />Use via the <MaterialIcon name="mic" /> helper that wraps <span className="material-symbols-outlined">.
The reference HTML files in stitch/ contain working Tailwind configurations and rendered layouts. Lift the Tailwind config and the JSX structure directly for the canonical screens. Then:
- Replace static data with props from the visual payload type
- Replace hardcoded text with i18n-ready strings (English-only for v1 but use the pattern)
- Replace static images with Scene7 URLs from the catalog
- Wire up event handlers to the API client
Do not redesign anything. If you find yourself wanting to change a layout or color, stop. The design is locked.
Read BUILD.md next for the sequenced milestones.