2× points on weekends · Gold tier
aiRA generated this promotion from your prompt, review the diff and edit or create.
Aira Design System v1.1 is organised around atomic design — Atoms → Molecules → Organisms → Templates — so every element is structured to map onto a Figma library. The product has one assistant voice (aiRA, shown only as the sparkle brand mark — no avatar), user avatars (initials), and custom-rendered form controls throughout.
Open this file in a browser. No build step. Toggle Light/Dark and LTR/RTL via the top bar.
Recurring copy fixes that keep aiRA's language fluent, honest, and on-voice. Pairs with Voice rules and Refusal grammar.
| Instead of | Write | Why |
|---|---|---|
| "As an AI, I'm not able to…" | "I can't pull that brand — RBAC restriction. Ask your admin to enable Analytics." | One aiRA voice; no meta-disclaimers. State the limit, then the path. |
| "I think this might work…" | "This sets a 2× multiplier for Gold on weekends." | Hedging reads as unreliable. Say what it does. |
| "Error: NullPointerException" | "I couldn't reach the analytics database. Retry, or contact support." | Plain-language reason + next step; never a stack trace. |
| Title Case Buttons & Headings | Sentence case everywhere | System-wide sentence case. |
| "Are you sure?!" | "Go back a step? Changes in another step will update." | Name the consequence; no fake urgency. |
| "Let me generate that for you…" | "Drafting promotion config" (activity step) | Show the work as a state, not chatty filler. |
How design tokens reach code, and how aiRA reads the system to stay on-brand.
tokens/tokens.json (DTCG 2025.10) is the source. scripts/generate.js emits css/variables.css — CSS custom properties for light and dark.--aira-* prefix (e.g. --aira-bg-surface, --aira-sp-3) so the tokens coexist with Ant Design v3 variables in the same app without clashing. Components use the matching Aira* PascalCase prefix.bg-surface, text-secondary, rounded-md resolve to token values — components never hard-code hex, spacing, or radius.tokens.json publishes to Figma variables via Tokens Studio (see Figma bundle), so design, code, and Figma stay one source of truth.An MCP server exposes the tokens, the component map, and Code Connect to aiRA. When the assistant drafts or edits UI, it can reference real component names (AiraButton, AiraDataTable…), token values, and usage examples — so generated output stays on-system instead of inventing one-off styles.
AiraX names → props, variants, and the library each is built with.This documents every part of the Aira Design System v1.1 — markup, tokens, behaviour, README, changelog, the Figma bundle (tokens.json in DTCG 2025.10, component map, Code Connect), and the system-wide Component status table. The source is now split into the tokens, css, and html/components folders; see the README for the build and serve steps.
The designer reference (this file) and the developer reference (the Storybook React library) are one system in two surfaces. In code, components are named AiraX and built with:
@base-ui/reactclass-variance-authority · clsx · tailwind-mergetailwindcss-animatelucide-reactxstate · @xstate/react (conversation, approval, streaming)@tanstack/react-table · @tanstack/react-virtualrecharts · d3date-fnsEach component section carries an Engineering handoff block with its AiraX name, the libraries it uses, and a React/Tailwind snippet. The full engineering contract is in Conventions.
@aira/react library implements.tokens/tokens.json, then run node scripts/generate.js && node scripts/bundle.js.html/components/<id>.html; keep its AiraX name and Engineering-handoff block in step with the Storybook.A standalone, browser-openable design system for the aiRA conversational product. Atomic IA, one assistant voice, no agent-identity fiction, and custom form controls throughout.
open aira-design-system/index.html
No build step. Toggle Light/Dark and LTR/RTL from the top nav.
aira-design-system/
index.html # full design system page tokens.css # all design tokens (light + dark) CHANGELOG.md # v1.1 release notes README.md # this file assets/ # logo + fonts figma/ # tokens.json, figma-components.md, code-connect.json
Atoms Button · Icon button · Input · Textarea · Chip · Tag · Badge · Pill
Avatar · Citation chip · Tooltip · Kbd · Divider · Spinner · Skeleton
Molecules Field · Dropdown (single · multi · searchable × sm · md · lg) Checkbox · Radio (incl. disabled) · Toggle · Stepper · Progress Feedback control · Action row · Tabs · Breadcrumb · Popover Toast · Banner · Nudge card · Selection card
Organisms Chat message · Composer · Approval card (with Type-to-confirm) Code block · Inline diff · Agent activity (animated single-step + todo-style multi-step) · Conversation state machine · Modal · Sheet List row · Data table (column filters) · Card · Form · Change-log feed
Templates Empty · Loading · Errors · My Configs · Mobile · Email × 4
1. One aiRA. No named agents. No agent-config / qa / analytics / support / coder tokens, classes, badges, or avatars anywhere. 2. Custom-rendered form controls. Never the native <select>. 3. Stop, not disable. Agents are stopped via the composer Stop icon; they're never disabled. 4. There is no coder agent. aiRA exposes no separate "coder" agent — code output (SQL, scripts) comes from the one aiRA, never a distinct coding persona. No coder-agent tokens, badges, avatars, or labels anywhere.
The component library ships as @aira/react (see the developer Storybook). This file is the design source of truth; for teams consuming it directly:
index.html. Copy demo HTML verbatim into your product; replace inline values with your data binding.tokens.css. Import once at the app root. Never redefine a token locally.data-ds-* hooks at the bottom of index.html (Tabs, Dropdown, sidebar search, Type-to-confirm).The aiRA mark is a cluster of three four-pointed sparkles filled with the brand gradient (indigo-400 → sky-400). It is the app logo, launcher icon, and email/marketing brand mark — a logo element, not a conversational avatar or per-message speaking indicator.
The sparkle (aira-sparkle.svg) is the smaller, in-product mark — used on AI-generated badges and as the launcher icon. The full logo (aira-logo.svg) is identical geometry at a larger render size.
Apply the gradient to the aiRA mark itself — and only the mark.
Use the gradient on a button, an input, a banner, or any large surface. It overpowers content.
For an action button, use the primary indigo (solid). Keep the gradient on the aiRA brand mark (logo) only — never on UI controls.
Replace solid status colours with the gradient. Status reads as state; gradient reads as brand decoration.
The mark scales from 12 px (inline meta) to 64 px (marketing hero). Keep the sparkle on a single stroke weight; do not redraw at small sizes.
Reserve at least 25% of the mark's height as clearspace around all four sides. Nothing intrudes — no text, no other glyphs.
#6268FF) → sky-400 (#3BA7FF), drawn across the bounding box of all three sparkles together. On dark surfaces, shift to the lighter pair: indigo-300 (#8E97FF) → sky-300 (#5EA8FF).Apply the gradient to the sparkle path itself. The mark is the gradient.
Place the sparkle inside a circular gradient background. The shape is the sparkle, not a chip.
Use the on-dark gradient (indigo-300 → sky-300) when the mark sits on a dark surface.
Render the sparkle in a single flat colour. The gradient carries the brand.
Lock the geometry — the same three-sparkle cluster at every size, on every surface.
Add a stroke, shadow, glow, or container around the logomark.
Mixed case — lowercase "ai" leading, uppercase "RA" trailing — locked as one token: aiRA. Set in Roboto Medium. Never reorder the case; never spell it "AIRA". The wordmark is live type, not artwork — it is set in the running font, never traced or outlined.
Render in --text-primary on light surfaces and #FFFFFF on dark or on the brand gradient. The gradient never fills the wordmark itself — it is reserved for the sparkle.
ai, uppercase RA — together, one token. Never AIRA, Aira, or aira.--fw-medium, weight 500). Letter-spacing -0.015em, line-height 1.--text-primary on light; #FFFFFF on dark or on gradient. No gradient fill, no stroke, no shadow.Set the wordmark at 18, 24, 32, 48 or 64 px font-size. Use the smallest size that remains comfortably legible on the surface.
Reserve clearspace equal to the cap-height of the "R" on all four sides of the wordmark. Nothing intrudes — no body copy, no buttons, no other glyphs.
Keep ai lowercase and RA uppercase, together as one token.
Spell it AIRA, Aira, or aira. The case is the brand.
Pair the wordmark with the sparkle in the combination mark whenever the layout allows it.
Fill the wordmark with the brand gradient. The gradient lives on the sparkle alone.
The horizontal lockup pairs the sparkle and the wordmark into a single asset (aira-lockup.svg). It is the default presentation in product chrome — top bar, sign-in, email headers.
Sparkle on the leading side, wordmark trailing, with a gap of --sp-2 (8 px). The sparkle's square dimension matches the wordmark's font-size in pixels (a 32 px wordmark pairs with a 32×32 sparkle; a 24 px wordmark pairs with a 24×24 sparkle). Optically centre the sparkle to the wordmark's x-height — the sparkle cluster extends roughly 1–2 px above the cap line and 1–2 px below the baseline.
--sp-2 (8 px) at wordmark sizes ≥ 24 px. --sp-1 (4 px) below.aira-lockup.svg.Keep the horizontal lockup intact at every size ≥ 20 px. Use the sparkle alone below that.
Increase the gap, stack the elements vertically, or wrap the lockup in a stroke or box.
import { AiraLockup, AiraMark, AiraWordmark } from "@aira/react";
<AiraMark size={28} /> {/* sparkle only */}
<AiraLockup size={32} /> {/* mark + wordmark */}
<AiraWordmark />
Two-tier system: raw primitives at the base, semantic tokens on top. Product code references semantic tokens only.
indigo-600 (#3438C9) is the primary action colour — the fill used on every primary button across the system. Flagged below.
indigo-400 → sky-400. The single gradient used for the aiRA mark, send button, avatars, and any aiRA-anchored surface.
The backgrounds and the text colours that sit on them across the product shell — chat panel, artifact panel, cards, left rail, top bar and selected rows — in light and dark. Every value here is pixel-sampled from the product (light + dark) and mapped to a named semantic token; product code references the token, never the raw hex.
#FBFCFD · card #FFFFFF#FFFFFF#182135 · card #131B2C#131B2C| Region (in-app) | Token | Light | Dark |
|---|---|---|---|
| Chat panel (conversation) | --aira-surface-chat-bg → --aira-bg-canvas | #F6F8FA | #0E1424 |
| Artifact panel | --aira-surface-artifact-bg → --aira-bg-surface-alt | #FBFCFD | #182135 |
| Card / KPI tile · top bar · sidebar · composer | --aira-surface-artifact-card-bg → --aira-bg-surface | #FFFFFF | #131B2C |
| Left rail | --aira-surface-rail-bg → --aira-bg-surface | #FFFFFF | #131B2C |
| Selected row | --aira-bg-selected | #EEF0FF | #1F2A55 |
| Hover | --aira-bg-hover | #EEF1F4 | #1C2640 |
Measured on the card surface (#FFFFFF / #131B2C) where this text actually sits. WCAG AA needs 4.5:1 for normal text; AAA needs 7:1.
| Text token | Light hex | On light | Dark hex | On dark |
|---|---|---|---|---|
--aira-text-primary | #111827 | 17.8:1 · AAA | #F4F6FA | 15.9:1 · AAA |
--aira-text-secondary | #364152 | 10.5:1 · AAA | #C8CFDB | 11.0:1 · AAA |
--aira-text-tertiary | #6B7785 | 4.6:1 · AA | #94A0B4 | 6.5:1 · AA |
On the bare canvas (#F6F8FA) text-tertiary dips to ~4.3:1 — keep tertiary on cards, or step up to text-secondary for muted text placed directly on the canvas. text-placeholder stays an intentional sub-AA hint (see Accessibility).
The full semantic set as it ships in the developer's Storybook, grouped the same way — Background, Text, Border — so design and code read off one list. These resolve to the primitives above; product code references the token, never the raw hex. The Surfaces & on-surface text block above is the curated in-app view of these same tokens.
| Token | Light | Dark |
|---|---|---|
--aira-bg-default | #FFFFFF | #0E1424 |
--aira-bg-subtle | slate-50 #F6F8FA | slate-900 #111827 |
--aira-bg-muted | slate-100 #EEF1F4 | slate-800 #1F2937 |
--aira-bg-sunken | slate-100 #EEF1F4 | #0A101C |
--aira-bg-canvas | #F6F8FA | #0E1424 |
--aira-bg-elevated | #FFFFFF | #131B2C |
--aira-bg-inverse | slate-900 #111827 | #FFFFFF |
--aira-bg-brand | indigo-600 #3438C9 | indigo-400 #6268FF |
bg-default is the chat / app base — white in light, canvas-dark in dark (it's what --aira-surface-chat-bg resolves to). bg-elevated is the raised card surface (aliases the internal bg-surface, same value).
| Token | Light | Dark |
|---|---|---|
--aira-text-primary | slate-900 #111827 | #F4F6FA |
--aira-text-secondary | slate-700 #364152 | #C8CFDB |
--aira-text-tertiary | slate-500 #6B7785 | #94A0B4 |
--aira-text-link | indigo-600 #3438C9 | indigo-300 #8E97FF |
--aira-text-inverse | #FFFFFF | slate-900 #111827 |
--aira-text-disabled | slate-400 #9AA4B2 | #5A6678 |
--aira-text-danger | red-500 #B11E1E | #F58B8B |
--aira-text-success | green-600 #185C32 | #6FD995 |
--aira-text-warning | amber-500 #8A5500 | #F0B95B |
Contrast for primary/secondary/tertiary is in the On-surface text · contrast table above. On bg-default (verified): text-link 8.2, text-success 8.0, text-danger 6.8, text-warning 6.2 (light) — all clear AA. text-inverse is for inverse surfaces only — it flips opposite the text-on-* tokens, so don't substitute one for the other. text-disabled is an intentionally low-contrast, WCAG-exempt disabled state — never use it for active text.
| Token | Light | Dark |
|---|---|---|
--aira-border-subtle | slate-100 #EEF1F4 | #1F2940 |
--aira-border-default | slate-200 #E2E6EB | #2A3654 |
--aira-border-strong | slate-300 #C9D0D9 | #3A4870 |
--aira-border-brand | indigo-600 #3438C9 | indigo-400 #6268FF |
border-brand is for brand-emphasis edges — it is not the focus ring (--aira-border-focus, indigo-300 in dark).
Reference semantic tokens (--aira-text-primary, --aira-surface-chat-bg) in product code.
Reference primitives (--aira-slate-700) or paste raw hex. They change between modes.
Keep muted (tertiary) text on cards, where it clears AA in both modes.
Put small tertiary text straight on the canvas — it falls below 4.5:1 in light.
Colors are authored in sRGB. Reference rendering is on a display calibrated to sRGB IEC61966-2.1, white point D65, gamma 2.2, at roughly 120 cd/m².
On uncalibrated panels — most Windows laptops, many external monitors — variation is expected in both light and dark mode. In light mode, panel white points drift cooler than D65, saturated mid-tones lose punch on sRGB-only panels, and hairline borders either vanish or harden. In dark mode, backlight bleed lifts near-black values into muddy grey-blue on IPS LCD while OLED renders them as nearly-off pixels. Four factors stack: display gamut (Mac displays cover P3; most Windows panels cover only sRGB), gamma curves, backlight characteristics, and browser color management (Safari respects the system ICC profile; Chrome and Edge on Windows are inconsistent).
#0E1424 — lifted slightly from true black so panels with backlight bleed render it as clearly dark rather than muddy grey-blue.Ambient lighting, blue-light filters, panel hardware, OS calibration, and browser color-management quirks are outside the system's reach. Aim for consistency within the variance envelope each panel allows.
Roboto, Regular (400) + Medium (500) + Semibold (600). One ramp, used consistently.
4 px base grid. Use the scale; no 10 px, 13 px, 22 px.
Three durations. prefers-reduced-motion halts animation — does not speed it up.
| Token | Value | Use |
|---|---|---|
--motion-fast | 120ms · ease-out | Hover, focus, micro-toggles. |
--motion-base | 200ms · ease-out | Default for most state changes. |
--motion-slow | 320ms · ease-out | Large surfaces (sheets, modals). |
--motion-reduced | 0.001ms | Detectable by JS when reduced-motion is on. |
Icons are re-exported from lucide-react under aiRADS names via src/lib/icons.js. Defaults: size 20, strokeWidth 1.5, currentColor. Use the aiRADS name (e.g. Send, Regenerate, Workbook) — not the underlying Lucide name. AiraSparkle is the one custom mark (not in Lucide); the inline SVGs here are reference renders.
Every icon, when wrapped in an Icon button (or any interactive target), supports four states. The matrix below shows the contract on a representative icon — the same treatment applies to every icon in the grid above.
| State | Plus | Send | Stop | Trash |
|---|---|---|---|---|
| Default | ||||
| Hover | ||||
| Pressed | ||||
| Disabled |
Default · hover · pressed · disabled apply to every interactive icon. Disabled is reserved for "this action is not currently possible". For aiRA generation, use the Stop icon instead of disabling Send.
Import icons from lucide-react by name and let colour inherit via currentColor, so the theme tints them.
Embed PNGs or bake hex colours into icons. Breaks dark mode.
Use the Stop icon (circle with a square inside) only when interrupting an in-flight aiRA generation.
Disable an agent. Agents stop generating; they're never disabled.
// re-exported from lucide-react under aiRADS names
import { Send, Regenerate, Workbook } from "@/lib/icons";
<Send /> {/* defaults: size 20 · strokeWidth 1.5 · currentColor */}
// use the aiRADS name (Send, Regenerate, Workbook), not the Lucide name
Five guidelines, the right encoding for every data relationship, and a gallery of every chart format aiRA produces. Based on Cleveland & McGill's perceptual rankings and Schwabish's Better Data Visualizations.
Encodings ranked by how accurately the eye reads them (Cleveland & McGill). Use higher-ranked encodings when precision matters.
| Rank | Encoding | Used in |
|---|---|---|
| 1 | Position on a common scale | Bar · Dot plot · Line |
| 2 | Position on non-aligned scales | Small multiples |
| 3 | Length without shared baseline | Stacked bar segments |
| 4 | Direction · slope | Slope chart |
| 5 | Angle | Pie chart (use sparingly) |
| 6 | Area | Treemap · Bubble |
| 7 | Volume | 3D — avoid |
| 8 | Colour saturation · hue | Heatmap · Choropleth |
| You're showing | Go-to chart types |
|---|---|
| Comparison across categories | Bar · Dot plot · Heatmap · Waffle |
| Change over time | Line · Area · Slope · Sparkline |
| Distribution | Histogram · Box plot |
| Relationship between variables | Scatterplot |
| Part-to-whole | Stacked bar · Treemap · Waffle |
| Precise value lookup | Data table (see below) |
The workhorse. Length on a common baseline reads accurately. Always start the value axis at zero. Sort by value unless a natural order exists.
Use horizontal when labels are long. Sort descending by default.
Two values per category, side by side. Limit to two groups; beyond that, switch to a slope chart or dot plot.
Total + composition. Only the bottom segment shares a baseline — put the most important there. Limit to three to five segments.
Bars extend in two directions from a central baseline. Use for data with a natural midpoint — satisfaction, sentiment, percent change.
Two dots per row, connected by a line. Shows the gap between two values across many categories — clearer than paired bars when the values are similar.
Grid of cells coloured by value. Two categorical axes, one quantitative measure. Sequential palette for a single measure; diverging when there's a midpoint.
10×10 grid of squares. Each square is one percent. More tangible than a pie for a single statistic — "23 of every 100 members".
The default for time series. Limit to four-to-six lines; beyond that, highlight-and-gray or small multiples.
When showing many series, color only the lines that carry the story. Everything else stays neutral.
Line chart with filled area below. Emphasises magnitude. Don't stack more than four-to-five series.
Two vertical axes connected by lines. Shows direction and magnitude of change across many categories at once.
Tiny inline chart — no axes, no labels. Used in tables and KPI tiles to add trend context to a single number.
Bars showing frequency per bin. Bars touch — gaps are reserved for categorical bar charts.
Median, IQR, and outliers in compact form. Requires statistical literacy — annotate parts for general audiences.
Dots on two continuous axes — the go-to for showing correlation. Use transparency when dots overlap; label outliers directly.
Nested rectangles, area encodes value. For hierarchical data. Area is imprecise — use for broad patterns, not lookup.
Only for two-to-three slices when the approximate proportion is the point. Never 3D, never exploded. With more slices, switch to a stacked bar or waffle.
Same constraints as a pie — two to three segments, single insight. The hole in the centre is a natural place to surface the headline number. Preferred over a pie when the dataset has a clear total worth calling out.
Unordered groups. Distinct, equally vivid hues. Limit to six.
WhatsApp is always green; Push is always indigo; Email is always teal. Users learn the taxonomy once. Push and Email use clearly different hues (indigo vs teal) — not two neighbouring blues — so they stay easy to tell apart, including under colour-vision deficiency.
For ordered data (low → high). Single hue varying in lightness.
For data with a natural neutral midpoint — sentiment, percent change. Two hues diverging from neutral.
Green for positive change · orange for negative change. Red is reserved for errors and destructive actions — never the default for "down" deltas in a chart.
Tables are visualisations too. Right-align numbers, light horizontal rules only where needed, sparklines for trend context.
| Store | Sales | Δ vs prev | 6-mo trend |
|---|---|---|---|
| HSR Layout | ₹14,21,540 | +8.2% | |
| Indiranagar | ₹11,03,210 | +2.1% | |
| Koramangala | ₹9,84,720 | −4.6% | |
| Whitefield | ₹7,21,310 | −11.8% |
The title is the takeaway. "Active members grew 14% over six months" beats "Active members over time".
Start every bar chart's value axis at zero.
Truncate a bar axis to amplify the difference — it misleads.
Use orange for negative deltas. Red is reserved for errors.
Use a pie chart with more than three slices.
Highlight-and-gray when there are more than four to six series.
Use 3D charts or gradient fills on chart marks.
import { AiraBarChart, AiraMetricTile } from "@aira/react/charts";
<AiraBarChart data={data} xKey="store" yKey="sales" />
<AiraMetricTile label="Active members" value={12481} delta={+4.2} />
// 17 chart components wrap recharts; scales/axes/interpolation from d3
Six lifecycle states with paired surfaces.
Single-tone palette; no carnival-coloured highlighting. Tokens used in Code block.
| Token | Used for |
|---|---|
--syntax-keyword | SELECT, function, const |
--syntax-string | 'literal' |
--syntax-number | 42, 3.14 |
--syntax-comment | // remark |
--syntax-operator | =, +, AND |
--syntax-function | COUNT(), SUM() |
--syntax-punctuation | (), ;, , |
Honour the device notch and home indicator on mobile surfaces (sheets, app bars).
The hatched red zones map to env(safe-area-inset-*) on iOS and to WindowInsets (system bars + display cutouts) on Android. On the web, the same --safe-* tokens cover both platforms — the browser resolves them per OS. Apply the tokens as padding on any container that touches the viewport edge — sheets, app bars, full-screen modals — so content clears the notch, punch-hole, home indicator, and gesture-nav pill on every orientation.
| Token | Source (web) | iOS maps to | Android maps to |
|---|---|---|---|
--safe-top | env(safe-area-inset-top, 0px) | Notch / Dynamic Island | Status bar inset (incl. punch-hole) |
--safe-bottom | env(safe-area-inset-bottom, 0px) | Home indicator | Gesture nav pill (or 3-button nav) |
--safe-left | env(safe-area-inset-left, 0px) | Landscape notch side | Display cutout (landscape) |
--safe-right | env(safe-area-inset-right, 0px) | Landscape home-indicator side | Gesture nav (landscape) |
On Android WebView (Chrome 88+), env(safe-area-inset-*) resolves once the app sets WindowCompat.setDecorFitsSystemWindows(window, false) and the manifest declares android:windowLayoutInDisplayCutoutMode="shortEdges". The CSS contract is identical to iOS — write tokens, not platform branches.
Pad sheets, app bars, and full-screen modals with padding-top: var(--aira-safe-top) and padding-bottom: var(--aira-safe-bottom) so content clears the notch, punch-hole, home indicator, and gesture-nav pill.
Hard-code 44 px or 34 px insets. They differ per device, OS, and orientation — use the env-backed tokens.
Test on iOS (Dynamic Island + home indicator) and Android (status bar + gesture nav). The tokens resolve correctly only when the Android host turns off "fit system windows".
Branch CSS by user-agent. The same --safe-* tokens cover both platforms — branching means a missed device.
Same tokens, retuned for touch. Scale up the hit targets, drop the density, and keep the visual rhythm consistent across Windows, macOS, iPadOS, and Android.
Mobile-first. Layouts collapse top-down.
| Token | Range | Columns |
|---|---|---|
sm | 0 – 599 px | 4 |
md | 600 – 1023 px | 8 |
lg | 1024 – 1439 px | 12 |
xl | 1440 px + | 12 |
Minimum tappable area per surface. Always pass the largest constraint that applies.
Headings step down one notch on sm; body stays.
| Role | Web | Tablet | Mobile |
|---|---|---|---|
| Display | 40 / 48 | 32 / 40 | 28 / 36 |
| H1 | 28 / 36 | 24 / 32 | 22 / 30 |
| H2 | 22 / 30 | 20 / 28 | 18 / 26 |
| Body | 14 / 20 | 14 / 20 | 15 / 22 |
| Caption | 12 / 16 | 12 / 16 | 12 / 16 |
Tokens are platform-agnostic. The only platform-aware pieces are scrollbar width, native focus-ring colour, and font-smoothing — all handled by the design system shell.
| Concern | Web (Win / Mac) | iOS / iPadOS | Android |
|---|---|---|---|
| Tap target | 36 px | 44 × 44 pt | 48 × 48 dp |
| Body text | 14 / 20 | 15 / 22 (Dynamic Type) | 14 / 20 (sp) |
| Corner radius | --radius-md 8 px | 10 px (continuous) | 12 px |
| Elevation | --shadow-sm / md / lg | same shadow tokens | same + state-layer overlay |
| Focus ring | 2 px indigo + 3 px halo | system tint | 2 dp indigo |
| Iconography | 1.5 px stroke @ 20 px | 1.5 pt stroke @ 24 pt | 1.5 dp stroke @ 24 dp |
| Safe area | — | env(safe-area-inset-*) | system bars · 56 dp top |
How the aiRA chat surface restructures across breakpoints.
Use the same semantic tokens at every breakpoint. Only the hit-target size and the type-scale notch change.
Re-skin a component per breakpoint. The tokens already handle theme; breakpoints only resize.
Bump every tappable target to ≥ 44 px on mobile and ≥ 48 dp on Android.
Ship a 28 px target outside a dense table. 28 px is the floor for opt-in density only.
Voice is part of the design. Nine hard rules.
"Couldn't run that query — the table isn't connected yet. Connect it under Settings → Data."
"Sorry, sorry — I'll try again." Reflexive apologies clutter the surface.
"aiRA created 1 promotion." Numerals scan faster than spelled-out one.
"aiRA thought you might like this!" Reads as artificial enthusiasm.
can't / can / nextWhen aiRA can't fulfil a request, the response has three parts: what it can't do, what it can, and what to do next.
Four variants, three sizes. Primary indigo-600 fill. The single canonical button atom — no per-context one-offs.
| Prop | Values |
|---|---|
variant | primary · secondary · tertiary · danger |
size | sm · md · lg |
disabled | boolean |
Use primary for the single confirming action; secondary for the alternative; tertiary for low-frequency.
Stack two primary buttons next to each other. There can only be one.
Match button size to the surrounding density.
Use a danger button for routine destructive actions like Remove — use secondary.
import { AiraButton } from "@aira/react";
<AiraButton variant="primary" size="md">Save</AiraButton>
<AiraButton variant="secondary">Cancel</AiraButton>
// variant/size resolved by class-variance-authority; classes merged with tailwind-merge
Single-icon affordances. 32 px default; 44 px touch. Use the Stop icon only for halting an in-flight aiRA generation.
The same Stop icon as the default state — a circle with a filled square, on the neutral icon-button surface. Indicates active generation; clicking it stops aiRA. No indigo fill: the generating state must not look like a primary action.
Pair every icon button with an accessible name describing the action.
Pack more than three icon buttons in a row without visual separation.
import { AiraIconButton } from "@aira/react";
import { Upload } from "lucide-react";
<AiraIconButton aria-label="Upload" size="md"><Upload /></AiraIconButton>
36 px default. Ten states.
Show a label above the input. Placeholder is never a substitute for a label.
Trigger validation on every keystroke. Validate on blur or submit.
Pre-fill the most likely value when one exists.
Use a generic Save error — name the field that failed.
<AiraInput value={v} onValueChange={setV} placeholder="aiRA" invalid={!!error} />
Multi-line input. Resizable on the Y axis only.
Resize vertically only.
Auto-grow past six lines without showing a scroll affordance.
<AiraTextarea rows={3} value={v} onValueChange={setV} />
Neutral metadata tag, removable. Carries an optional dot for status semantics.
Reserve colour semantics — green for success, amber for pending, red for danger.
Stack chips, tags, and badges in the same row — pick one primitive per context.
<AiraChip tone="success">Live</AiraChip>
Categorical label. Larger surface than a chip; no dot.
Use Tag for categorical grouping (channel, type, owner).
Use Tag for status — that's a Pill.
<AiraTag>Beta</AiraTag>
Count or numeric indicator. Sits on top of a parent surface.
Cap counts at 99+. Beyond that, the precise number is noise.
Use a badge for a state name — that's a Pill.
<AiraBadge count={3} />
Lifecycle states across approvals, scheduled jobs, and content. Six variants.
| Variant | Token pair |
|---|---|
draft | --approval-draft-bg · --approval-draft-text |
awaiting | --approval-awaiting-bg · --approval-awaiting-text |
upcoming | --approval-upcoming-bg · --approval-upcoming-text |
approved | --approval-approved-bg · --approval-approved-text |
live | --approval-live-bg · --approval-live-text |
rejected | --approval-rejected-bg · --approval-rejected-text |
Use the canonical state colours so users learn the lifecycle once.
Invent new state labels — extend the existing set instead.
<AiraPill status="upcoming">Upcoming</AiraPill>
Avatars are for the human user only — initials on the canonical gradient (--avatar-grad). aiRA has no avatar: its visual identity is the sparkle brand mark (see Logo & marks) — a logo element, never a per-message speaking avatar or profile image.
Use avatars for the human user only. aiRA has no avatar — its identity is the sparkle brand mark.
Render the sparkle mark as a speaking avatar or profile image beside aiRA's messages. It's a brand mark, not a character.
Provide aria-label when there's no visible name.
Use random colours for user initials. The system has one avatar gradient.
<AiraAvatar name="Punya Shetty" size="sm" />
{/* user avatars only — aiRA has no avatar; use <AiraMark /> for the brand mark */}
RAG provenance. Real <a href> links. Hover lifts the border + text to brand.
Tier upgrades happen automatically when 90-day spend crosses the threshold 1Tier upgrade SOP. The system re-evaluates eligibility nightly 2Loyalty engine spec.
Hover or keyboard-focus brightens the border (--citation-border-hover) and shifts the text to brand colour (--citation-text-hover).
Customer 88959517 wasn't issued points because the transaction amount was below the promotion's minimum threshold.
Number citations sequentially in reading order.
Cite more than four sources inline. Use the appended list.
<AiraCitationChip index={1} href="#source-1">Proposal #4421</AiraCitationChip>
Short explainer on hover/focus. Surface is --tooltip-bg (slate-700 — softer than near-black) with white text at about 10 : 1 contrast.
Use Tooltip for short, optional context. Trigger on hover AND focus.
Hide essential information inside a tooltip. Surface it inline.
<AiraTooltip content="Copy">
<AiraIconButton aria-label="Copy"><Copy /></AiraIconButton>
</AiraTooltip>
// wraps @base-ui/react Tooltip; enter/exit via tailwindcss-animate
Visualises a key in a shortcut. Use sparingly — inline help, command palette docs, the Selection card keyboard hint.
Always document both platforms. Mac uses ⌘ (Command) and ⌥ (Option); Windows uses Ctrl and Alt. Detect the user's OS and show the matching row at runtime; list both in docs.
| Action | Mac | Windows |
|---|---|---|
| Command palette | ⌘ K | Ctrl K |
| Slash commands | / | / |
| Send message | ⌘ Enter | Ctrl Enter |
| New line in composer | ⇧ Enter | Shift Enter |
| Open help / shortcuts | ⌘ / | Ctrl / |
| Stop generation | Esc | Esc |
<AiraKbd>⌘</AiraKbd> <AiraKbd>K</AiraKbd>
1 px horizontal rule using --border-default.
<AiraDivider /> {/* @base-ui/react Separator */}
Indeterminate progress. 14 px default.
<AiraSpinner size="sm" /> {/* animate-spin */}
Placeholder for loading content. Hide from assistive tech (aria-hidden).
<AiraSkeleton className="h-4 w-40" /> {/* shimmer */}
Composed label + control + helper + error. The shape every form control uses.
label[for] ↔ input[id].aria-describedby to associate the helper with the input.aria-describedby in addition to the visual border colour.<AiraField label="Email" hint="Work address" error={error}>
<AiraInput name="email" />
</AiraField>
// @base-ui/react Field wires label, description & validity
The canonical custom-rendered form-select molecule. Three variants (single · multi · searchable) × three sizes (sm · md · lg) — maps directly onto a Figma 3 × 3 variant matrix. Never use the native <select>; behaviour is fully controlled in CSS and JS.
Single-choice selection. Caret on the right; menu opens below.
Checkbox rows inside the menu; trigger shows a count chip. Closing commits the selection.
Input-driven; type to filter. The menu shows a search input above results when the option set is large.
Open menus sit at z-index: 40 over sibling content. Max-height is 280 px; longer lists scroll internally. Menus never push the section below off-screen, and they always render above later content — fixing the v2 combobox overlap bug.
| Prop | Values |
|---|---|
variant | single · multi · searchable |
size | sm · md · lg |
state | closed · open · loading · empty · disabled · error |
options | Array of { id, label, sublabel } |
onChange | Single: option id. Multi: array of ids. |
aria-haspopup="listbox" + aria-expanded.role="listbox" with aria-multiselectable on multi.role="option" with aria-selected.Use sm in dense tables; md as default; lg only in marketing forms.
Default to 'Select…' when a sensible pre-pick exists.
Cap the open menu at the molecule's max-height; never let it cover the next section.
Use multi-select when the user picks one of two — that's a Toggle.
<AiraDropdown items={members} value={id} onValueChange={setId} searchable />
// @base-ui/react Select/Combobox; long lists virtualized with @tanstack/react-virtual
Multi-select within a group. Wrap with <fieldset> + <legend>.
Wrap a group with a fieldset and a legend so the group has a name.
Use a single standalone checkbox for binary on/off — that's a Toggle.
<AiraCheckbox checked={on} onCheckedChange={setOn}>Email me</AiraCheckbox>
Exclusive selection. Always wrap in <fieldset> + <legend>. The disabled state is documented explicitly.
Always pre-select the safest, most-common default.
Use radio for more than five options. Switch to a Dropdown.
<AiraRadioGroup value={v} onValueChange={setV}>
<AiraRadio value="gold">Gold tier</AiraRadio>
</AiraRadioGroup>
Binary on/off that takes effect immediately. For settings that need an explicit Save, use a Checkbox in a form.
Reflect immediate effect — no Save button.
Use a Toggle when the change needs a confirmation. Use a Modal.
<AiraToggle checked={on} onCheckedChange={setOn} /> {/* @base-ui/react Switch */}
Ordered multi-step flow.
<AiraStepper value={n} min={0} max={10} onValueChange={setN} />
Determinate progress for known durations.
<AiraProgress value={62} /> {/* @base-ui/react Progress */}
Two bare icon buttons — thumbs-up means like, thumbs-down means dislike — sitting inline with the action row. No pill, no container, no divider. The selected thumb fills with the link colour. The signal is recorded inline on click; thumbs-down does not open a reason picker.
Render the thumbs as bare icon buttons, sitting inline with the action row. No pill, no border, no divider.
Wrap the controls in a chip, pill, or bordered container. The icons themselves are the affordance.
Record the like / dislike signal inline and immediately. The thumb fill is the only confirmation the user needs.
Open a reason picker or modal prompt on dislike. The thumb signal is enough — don't interrupt the user to ask why.
import { ThumbsUp, ThumbsDown } from "lucide-react";
<AiraFeedback value={vote} onValueChange={setVote} />
// thumbs = like / dislike; dislike records inline, no reason picker
Inline actions under an aiRA reply — Copy · Support · Feedback (thumbs-up = like, thumbs-down = dislike). Support opens help; thumbs-down records the signal inline and does not open a reason picker. Keyboard focus uses the global :focus-visible ring; no per-action override.
<AiraActionRow onCopy={copy} onSupport={openHelp} value={vote} onVoteChange={setVote} />
// Copy · Support · Feedback — icon-only (lucide-react)
Local navigation between views of the same data.
<AiraTabs value={tab} onValueChange={setTab}
items={[{ id: "result", label: "Result" }, { id: "sql", label: "SQL" }]} />
// @base-ui/react Tabs (roving focus, aria-controls)
Hierarchical location indicator. Useful inside multi-step configurators.
<AiraBreadcrumb items={[{ label: "Home", href: "/" }, { label: "Loyalty" }]} />
Rich content with actions. Footer buttons are right-aligned; primary on the far right.
The Churn Propensity model rates this customer at 0.74 based on recency (45 days), basket-size decline (−32% qoq), and no email opens in 14 days.
Popover footer actions are right-aligned. Primary action sits on the far right. Tertiary or destructive actions sit to its left.
Right-align footer actions; primary on the far right.
Hide essential information in a popover. Surface it inline.
<AiraPopover trigger={<AiraButton>Filters</AiraButton>}>…</AiraPopover>
// @base-ui/react Popover; positioned with its anchor
Transient notification. Auto-dismisses; reserves for non-critical confirmations.
Auto-dismiss after about four seconds.
Stack more than three toasts. Collapse the rest into a notification centre.
const { toast } = useAiraToast();
toast({ title: "Promotion created", tone: "success" });
// @base-ui/react Toast provider
Inline page-level message. Persistent until dismissed.
Match the banner colour to severity — info, warning, danger, success.
Use a banner for transient feedback. That's a Toast.
<AiraBanner tone="warning">Multi-brand mode is on.</AiraBanner>
Suggestion or prompt presented next to the chat surface. Blue left-border accent.
Render adjacent to the chat surface, never on top of it.
Stack two nudges. One per session.
<AiraNudgeCard title="38% of top customers spend on Saturdays" onDismiss={hide} />
Disambiguation step. Keyboard-first, escape-to-skip, and — when the listed options don't fit — a free-text composer the user can open and type a full paragraph into. Option rows and the "Something else" row use visually distinct hover treatments so the two never look like the same target.
2–5 numbered interpretations. Click a row, press 1–5, or arrow + Enter. "Something else" opens the free-text box below — it never forces a listed answer.
When the user picks "Something else," the card swaps the list for a composer. It opens at two lines and auto-grows as they type, up to ~7 lines, then scrolls internally — so a one-liner and a full paragraph both fit without the card jumping. Click in and type; Send enables once there's text.
The same composer holding a multi-line paragraph. It has grown to fit the content; past ~7 lines it caps and scrolls inside the field, and Send is enabled because there's non-empty text. Edit it freely — it re-flows as you go.
When aiRA can't enumerate sensible choices, skip the list entirely and lead with the composer. Same control, no numbered rows — for "describe it in your words" moments.
| Variant | When to use | Composer |
|---|---|---|
| Pick one | 2–5 likely interpretations | Collapsed — opens from "Something else" |
| Free text | User picked "Something else" | Expanded, empty, auto-grows |
| Free text · filled | Reviewing / editing a typed answer | Holds a paragraph; scrolls past ~7 lines |
| Open-ended | Options can't be enumerated | Composer only, no list |
Option rows and "Something else" use different hover surfaces so they read as separate affordances; the free-text field is a real, focusable input.
| Element | Default | Hover | Focus (keyboard) | Selected / active |
|---|---|---|---|---|
| Option row | transparent | --aira-bg-hover | focus-visible ring | --aira-bg-selected + filled number badge |
| "Something else" | transparent, 1 px top divider | --aira-bg-accent-subtle (distinct violet tint) | focus-visible ring | aria-expanded="true" — reveals the composer |
| Free-text field | 2 lines, placeholder | n/a | focus ring on the field (focus-within) | grows with content to ~7 lines, then scrolls |
| Send | disabled (empty) | --aira-btn-primary-bg-hover | focus-visible ring | enabled on non-empty text |
| Disclosure chevron | points down (collapsed) | row hover applies | row focus-visible ring | rotates 180° when the composer is open |
| Dismiss (×) / Esc | header icon, top-right | --aira-bg-hover | focus-visible ring | closes the card (skips the question) |
| Prop | Values |
|---|---|
title | The question being asked |
options | Array of 2–5 strings. Omit for the open-ended variant. |
step | { current, total } when chained |
allowFreeText | boolean (default true) — "Something else" opens the composer |
freeTextPlaceholder | string — placeholder shown in the empty composer |
maxRows | number (default 7) — rows the box grows to before it scrolls |
submitOnEnter | boolean (default true) — Enter sends, Shift+Enter adds a line |
onSelect | Fires on click, Enter, or numeric key (1–5) |
onSubmitText | Fires on Send or Enter with the typed paragraph |
onSkip | Fires on Esc or the × (dismiss) — skips the question |
Use when the prompt has 2–5 likely interpretations. Numbered options are faster than prose.
Use for more than 5 options. Switch to a Dropdown (searchable).
Always offer free text and a way out (× / Esc). "Something else" must open a real, focusable input — never a dead end.
Cap the box at one line or truncate. Let it grow with the answer and scroll past ~7 lines.
Keep the composer in the card. Typing here is the answer to this question.
Block the main composer while a Selection card is up. Typing there dismisses the card.
<AiraSelectionCard
title={question}
options={options} {/* omit for open-ended */}
value={choice} onValueChange={setChoice}
allowFreeText freeTextValue={text} onFreeTextChange={setText}
maxRows={7} submitOnEnter
onSubmitText={handleSubmit} onSkip={handleSkip}
/>
Interactive checklist with check-off. Items strike through and fade once complete. Used in onboarding, multi-step setup flows, and anywhere a small set of discrete tasks needs visual progress.
input[type="checkbox"].Use for short, ordered lists (3–8 items) where each row is a discrete task.
Use for long, unstructured task lists. Past ~8 items, switch to a Data table with status.
Animate the strike-through on toggle (200 ms). Builds visual confirmation.
Hide completed items by default. Crossed-out items show progress and remind the user what was done.
<AiraTodoList items={items} onToggle={toggle} />
// progress label derived from checked count
Three message types. aiRA's reply free-flows as plain text — no avatar, no aiRA sparkle mark, no agent label, no badge, no chip anchoring the start of the message.
role="log" aria-live="polite"; new messages announce.role="status" aria-live="polite".role="alert".Let aiRA's reply free-flow as plain text. No avatar or sparkle mark — the prose stands on its own.
Anchor aiRA's reply with an avatar, sparkle mark, bubble, or agent-name badge. There are no agent labels and no avatar.
<AiraChatMessage role="user">Show last month’s sales by store.</AiraChatMessage>
<AiraChatMessage role="ai">{markdown}</AiraChatMessage>
// roles: user · ai · system · error. aiRA reply free-flows (no avatar). Times via date-fns
Input area with attach, voice, send. While aiRA is generating, the send icon swaps for the Stop icon — clicking it interrupts generation and keeps the partial output. Agents are never disabled; they're stopped.
This is the conversation's idle state: the input is auto-focused with the caret blinking, so the user can start typing immediately — no click needed. The input itself is the live region. The Send button stays disabled until the user types; disabled uses the neutral disabled surface, and the gradient never lives on this control.
Once the input has content, Send enables and renders in the primary action colour (indigo-600). No gradient — gradients are reserved for the aiRA mark.
The send icon is replaced by the enabled Stop icon. Placeholder reads "Generating…". Clicking Stop transitions the conversation state to aborted; partial output is preserved.
For historical or shared sessions where the user can read but not send.
Swap the Send icon for the Stop icon while aiRA generates.
Disable the composer because aiRA is busy. Let the user keep typing.
<AiraComposer
state={state.value} // idle | thinking | streaming
onSend={text => send({ type: "SEND", text })}
onStop={() => send({ type: "STOP" })}
/>
// Send swaps to Stop while streaming (driven by the conversation machine)
aiRA proposal awaiting human review. Header is the proposal title only — no agent badge. High-risk variants use the Type-to-confirm pattern.
aiRA generated this promotion from your prompt, review the diff and edit or create.
Six lifecycle states — these status variants are mainly for configurations. Status pill on the left of the footer; primary action shifts per state.
Draft — not yet submitted for approval.
Submitted 4 min ago · with Harsh Deo for review.
Approved by Harsh Deo · scheduled to go live Sat at 00:00.
Approved by Harsh Deo · 2 hours ago.
Live since Sat 00:00 · 1,247 redemptions so far.
Rejected by Harsh Deo · 2 hours ago.
Production-affecting, irreversible, security-relevant, or financial changes require a secondary confirmation. On Accept click, the footer Accept button swaps in place for a slim input (placeholder "Type APPROVE to confirm") + a Confirm button — both disabled until the input matches APPROVE (case-insensitive). Edit remains reachable. The Confirm button uses the primary indigo colour — not red — at the same compact size as a routine primary action; no giant outsized button.
Applies to all 1.2 M members; irreversible once live. Confirm by typing APPROVE.
| Lines in diff | Treatment |
|---|---|
| ≤ 12 | Render in full. No scroll, no collapse. |
| 13 – 40 | Max-height 320 px with internal scroll. Caption above the diff: "12 of 40 lines visible". |
| > 40 | Collapse to a summary (N added · M removed · K changed) with "View full diff" opening a Modal. Approval cannot complete until the full diff has been opened. |
| Binary payload | Replace diff with a before/after preview. Never paste raw bytes. |
audience or schedule expands. Surface a risk pill in the header.Show the diff or preview inline up to twelve lines; collapse beyond that.
Reuse the routine Accept styling on high-risk proposals. Use Type-to-confirm.
<AiraApprovalCard
proposal={proposal}
riskLevel="high" // swaps Create for type-to-confirm
onEdit={edit} onCreate={create}
/>
// lifecycle (draft→awaiting→upcoming→approved→live) via xstate; dates via date-fns
First-class output for SQL and scripts. Language badge and a Copy action.
function normaliseRow(row) { // Drop fields not in target schema const { id, amount, currency } = row; return { id, amount: Number(amount), currency: currency || 'INR' }; }
<AiraCodeBlock lang="javascript" showCopy>{code}</AiraCodeBlock>
A line-by-line before → after view of a change aiRA proposes, so a reviewer can see exactly what moves before approving. Red lines marked − are being removed; green lines marked + are being added; plain lines are unchanged context. Used inside Approval cards and the Change-log feed.
points_multiplier from 1.5 to 2.0 — read it top-to-bottom as old → new.+ line is brand-new. Line 4 adds free_shipping; nothing was removed.−/red, +/green), so the diff still reads in greyscale and for colour-vision deficiency — never colour alone.Pair sign and colour — plus for added, minus for removed.
Hide context lines. Surrounding context tells the reviewer where the change lands.
<AiraInlineDiff before={oldYaml} after={newYaml} />
// line diff (d3-array); +/− paired with colour for grayscale & CVD
Streaming disclosure of what aiRA is doing. Use the single-step row for one task; the multi-step list when several steps run in sequence.
One row: spinner + step verb + optional detail. Use plain verbs — no internal job IDs.
Each step starts empty; the indicator fills on activation and locks to a check mark when complete. Completed rows strike through with faded text, like ticking items off a list.
Use plain verbs. "Querying analytics DB" — not "Executing query_engine_v3".
Surface internal job IDs.
Show duration on completed steps.
Spin forever. Past 10 s, append "Still working — taking longer than usual" inline.
<AiraAgentActivity steps={steps} current={i} />
// reflects thinking / streaming states from the conversation machine
An AI turn has seven stops. The table below maps every state to its visible UI, user affordance, and live-region behaviour.
| State | Visible UI | User affordance | Live region |
|---|---|---|---|
| idle | Composer auto-focused; caret blinking in the empty input. Send button stays disabled while the input is empty. | Just start typing — no click needed. | The input area itself: focused with the caret blinking, signalling the user can type immediately. |
| user-sent | User message renders end-aligned. Composer clears, send returns to disabled. | Continue typing follow-ups. | Thread role="log" announces. |
| thinking | Activity row above the composer — spinner/sparkle + step verb. | Cancel on the activity row (Esc alias). | role="status" aria-live="polite". Step changes announce. |
| streaming | aiRA message renders; tokens append in place. Composer Send is replaced by the enabled Stop icon. | Click Stop to interrupt (preserves partial output). | Body role="status"; tokens announced in chunks ≥ 80 chars. |
| done | Final reply renders; Action row appears (Copy · Support · Feedback). | Compose a follow-up — loops back to user-sent. | Live region settles. |
| error | Error message in the danger surface. Plain-language reason. | Retry (rewinds to user-sent with original prompt). Optional Report. | role="alert" — interrupts. |
| aborted | Partial reply stays; system line: "Stopped at your request." | Continue from here · Regenerate · ask a new question. | System line not announced. |
Each transition is driven by exactly one trigger. Read every row as: from this state, this trigger moves the turn to the next state.
| From | Trigger | To |
|---|---|---|
| idle | User types, then sends | user-sent |
| user-sent | aiRA begins working | thinking |
| thinking | First content token arrives | streaming |
| thinking | Cancel (or Esc) | aborted |
| streaming | Stop clicked — partial output kept | aborted |
| streaming | Last token arrives | done |
| thinking · streaming · aborted | Something fails | error |
| error | Retry — re-sends the original prompt | user-sent |
| done · aborted · error | User composes a follow-up | user-sent |
Two rules hold across every transition:
import { useMachine } from "@xstate/react";
import { conversationMachine } from "@aira/react/machines";
const [state, send] = useMachine(conversationMachine);
// states: idle → userSent → thinking → streaming → done | aborted | error
// transitions are the single source of truth for composer + activity + chat
For confirmation, destructive actions, validation prompts, and short forms.
Use when the user is about to do something destructive, irreversible, or financially material. Icon-led headline, plain-language consequence, danger primary action.
Use when leaving a step would change or discard work already entered elsewhere. State the consequence plainly and let the user choose to stay or go back.
Use when the user tries to advance with missing or invalid fields. Name exactly what needs attention; one primary action takes them back to fix it. Nothing is discarded.
alertdialog (not dialog) so assistive tech announces it as a warning.Use sparingly — reserve for confirmation or short forms.
Nest modals inside modals.
<AiraModal open={open} onOpenChange={setOpen} title="Send for approval?">…</AiraModal>
// @base-ui/react Dialog; role="alertdialog" for destructive / validation
Mobile escalation of Modal. Honours --safe-bottom.
Pad the bottom with safe-bottom so the action row clears the home indicator.
Stack content beyond 95% height — route to a page instead.
<AiraBottomSheet open={open} onOpenChange={setOpen}>…</AiraBottomSheet>
// @base-ui/react Dialog (bottom placement); honours safe-area insets
Chat history rail — a narrow sidebar pinned to the left of the chat surface listing previous sessions (a common chat-history convention). Each row is a single click target. An export marker on the right shows how many times the session was exported (FTP push, CSS download, or workbook download).
+ in the "Sessions" header starts a new chat. On hover it shows a clear background and a "New chat" tooltip; on click it opens a fresh session — a new active row is prepended to the top of the list and the previously active row is deselected. (The demo above is live — try it.)--bg-selected with a 3 px --border-focus leading border.Wrap the whole row in a single interactive target. The export marker is a tooltip, not a separate link.
Split actions into multiple click targets on the same row.
Show one pip per distinct export type. The count carries multiplicity.
Repeat the same pip three times for three exports of the same kind. The pip is the type; the count is the volume.
<AiraListRail>
{sessions.map(s => (
<AiraListRow key={s.id} title={s.title} time={s.time} active={s.id === current} />
))}
</AiraListRail>
// AiraListRail virtualizes long histories with @tanstack/react-virtual
Every column header carries the same sort icon — two opposing arrows (one down, one up). A click cycles ascending → descending → unsorted. The icon never changes shape; only its colour reflects state (slate-500 idle, indigo-600 active). aria-sort on the header carries the order for assistive tech. No filter icons in headers.
| Store | Sales | Txns | Δ vs prev 30d | Status |
|---|---|---|---|---|
| HSR Layout | ₹14,21,540 | 3,219 | +8.2% | Active |
| Indiranagar | ₹11,03,210 | 2,884 | +2.1% | Active |
| Koramangala | ₹9,84,720 | 2,440 | −4.6% | Active |
| Whitefield | ₹7,21,310 | 1,940 | −11.8% | Upcoming |
One icon, three states. Shape stays the same on every header; colour and aria-sort carry the meaning.
| State | Icon shape | Icon colour | aria-sort |
|---|---|---|---|
| Unsorted (idle) | sort | --text-tertiary (slate-500) | none |
| Ascending (active) | sort | --text-link (indigo-600) | ascending |
| Descending (active) | sort | --text-link (indigo-600) | descending |
Use the same sort icon on every column header. Consistency reads as one control, repeated.
Change the icon's shape when a column becomes active. Keep the two-arrow sort glyph and let colour + aria-sort carry the direction.
Right-align numbers; use font-variant-numeric: tabular-nums.
Use red for negative deltas. Orange (delta-negative) is the chart palette default.
Wire aria-sort on the header — assistive tech reads it.
Pair a separate filter icon with the sort. The data table sorts; it doesn't filter.
import { AiraDataTable } from "@aira/react";
<AiraDataTable columns={columns} data={rows} />
// sorting/columns via @tanstack/react-table; rows virtualized with @tanstack/react-virtual
Generic container. Use only when the content needs visual elevation; otherwise prefer a plain section.
1,247 active members · 38% redemption rate · 12% new tier upgrades.
Use Card only when the content needs elevation; otherwise a plain section is enough.
Nest cards inside cards.
<AiraCard title="Weekend boost" footer={<AiraButton>Open</AiraButton>}>…</AiraCard>
Standard composition — field stack + actions footer.
Keep the action footer right-aligned; primary on the far right.
Hide the submit affordance below the fold.
<form onSubmit={onSubmit}>
<AiraField label="Program name"><AiraInput name="name" /></AiraField>
<AiraButton type="submit">Save</AiraButton>
</form>
// validation surfaced through @base-ui/react Field
A product UI pattern for visualising change history inside aiRA — e.g. a loyalty-program edit timeline, a Config audit trail, or "What changed since you last looked." Vertical timeline with grouped entries.
audience; scope narrowed back to Gold tier.auto_apply flag from program settings.Group entries by day; lead with actor and change type.
Include entries with no summary. Every entry must say what changed.
<AiraChangeLogFeed entries={entries} groupBy="day" />
// relative timestamps via date-fns; long feeds virtualized with @tanstack/react-virtual
Specific copy that names what's missing and offers a next step. Never a generic "No results".
Try a shorter query, or search by promotion ID. You can also reset filters to see everything in this brand.
Three loading patterns. Choose by the work being done.
// streaming is a state machine, not a boolean
const [state] = useMachine(conversationMachine);
{state.matches("thinking") && <AiraAgentActivity … />}
{state.matches("streaming") && <StreamedTokens … />}
Errors are first-class UI. Plain-language reason, then what to do next. Never a stack trace.
A list of saved configurations the user can reuse — list-row organism with the config name and a status pill.
Desktop is the primary surface; mobile honours safe-area tokens for sheets and bottom CTAs. The mobile composer keeps the full attach · voice input · send affordances — voice input is first-class on mobile, not desktop-only.
<AiraBottomSheet open={open} onOpenChange={setOpen}>…</AiraBottomSheet>
<AiraComposer voiceInput /> {/* mic affordance is first-class on mobile */}
Tokens applied to the aiRA surface. The same theme switch flips every element below — sidebar, header, tabs, metric cards, alert — with no layout shift and no contrast loss. Toggle the Dark button in the top bar and watch the cascade.
Every surface above is composed from semantic tokens — --bg-surface, --bg-surface-alt, --bg-selected, --border-default, --text-primary, --text-tertiary, --text-link, --delta-positive, --delta-negative. Toggling the theme on the top bar re-resolves every one of those — no per-component override needed.
<div className="grid grid-cols-2 gap-4">
<AiraMetricTile label="Tier 1 spend" value="₹14.2L" delta={+12} />
<AiraSparkline data={trend} />
</div>
Four transactional templates aiRA sends. Shared rules apply to all four — listed once below.
tokens.css at build time.Sent when a user shares a chat session with a colleague.
Hi Punya,
Keshav (keshav.agrawal@acme.com) has invited you to view a chat session.
| Open session |
Sent when a reviewer is asked to approve an aiRA proposal.
Punya created this promotion using aiRA. It's waiting on your approval before it goes live this Saturday.
| Review |
If you can't get to this today, reply with defer 24h and aiRA will resend tomorrow.
Sent when a scheduled aiRA job completes (daily / weekly digest).
Your scheduled summary is ready.
|
Active members
12,481
+4.2%
|
Redemption rate
38%
−1.1%
|
|
|
Tier upgrades
219
|
Top store
HSR Layout
|
|
| Open in aiRA |
A morning summary of pending items the user owns.
Render diffs as text in the email. Survives mobile zoom, alt-text fallback, and assistive tech.
Send a "click to view in browser" stub. Put the decision in the email.
WCAG 2.2 AA is the floor for text and interactive elements. Key pairings are checked in light and dark below (ratios rounded). Placeholder text is the one intentional exception — it sits below AA as a supplementary hint, and every field carries a persistent visible label that meets the requirement.
| Pairing | Ratio | WCAG |
|---|---|---|
text-primary on bg-surface | 17.7 : 1 | AAA |
text-secondary on bg-surface | 10.3 : 1 | AAA |
text-tertiary on bg-surface | 4.6 : 1 | AA |
btn-primary-text on btn-primary-bg | 8.2 : 1 | AAA |
tooltip-text on tooltip-bg (slate-700 surface, white text) | 10.3 : 1 | AAA |
citation-text-hover on citation-bg-hover | 9.7 : 1 | AAA |
status-upcoming-text on status-upcoming-bg | 5.0 : 1 | AA |
text-placeholder on bg-surface | 2.5 : 1 | Hint only — see note |
| Pairing | Ratio | WCAG |
|---|---|---|
text-primary on bg-surface | 15.9 : 1 | AAA |
tooltip-text on tooltip-bg (slate-100 surface, slate-900 text) | 15.7 : 1 | AAA |
btn-primary-text on btn-primary-bg (indigo-500, white text) | 5.7 : 1 | AA |
status-upcoming-text on status-upcoming-bg | 8.0 : 1 | AAA |
focus-visible on every interactive element.prefers-reduced-motion halts animation (sparkle, todo strike-through, shimmer).for/id or aria-labelledby; errors via aria-describedby.aria-label.Five surfaces for telling the user something. Pick by urgency and persistence.
| Surface | Use when… | Persistence |
|---|---|---|
| Toast | Non-critical confirmation that the user can ignore. | Auto-dismisses ~4 s. |
| Banner | Page-level state that should persist. | Stays until dismissed or condition changes. |
| Nudge card | Suggestion adjacent to the chat surface; user can act or ignore. | Stays until dismissed. |
| Inline error | Field-level validation. | Stays until the field is valid. |
| Modal | The user must act before continuing. | Blocks the page until resolved. |
A command palette (⌘ K on Mac, Ctrl + K on Windows) and slash commands are expected conventions. The system doesn't ship a palette component yet because the underlying command surface isn't designed. Until that's decided, do not ship a command-palette UI per team — the design will fragment. Tracked under Deferred.
For multi-brand work, the developer-facing contract — token names, component names, and the override path — aligns strictly with the first iteration of the shared developed (Aira) design system. The design system follows the developed system's naming so the two never drift; don't introduce design-only multi-brand constructs ahead of that v1 contract. Multi-brand theming hooks stay Deferred until the first reseller deployment commits.
Components ship as a published React library — the developer Storybook is the live reference. This document is the design source of truth; the table below maps each concern to the library the build uses.
| Concern | Library |
|---|---|
| Accessible primitives — dialog, popover, tabs, select, tooltip, switch, field… | @base-ui/react |
| Variants & className | class-variance-authority · clsx · tailwind-merge |
| Icons | lucide-react |
| Conversation & approval state | xstate · @xstate/react |
| Tables & long lists | @tanstack/react-table · @tanstack/react-virtual |
| Charts | recharts · d3 |
| Dates & relative time | date-fns |
| Enter / exit animation | tailwindcss-animate |
AiraX (PascalCase). Co-locate its cva variant definition and exported prop types.forwardRef to the root element and spread …props so consumers can pass DOM attributes.xstate machine — not in ad-hoc component state.variant / size / tone resolved by class-variance-authority; the consumer's className is merged last with tailwind-merge, so overrides win without specificity fights.value + onValueChange (the @base-ui/react convention), not onChange with raw events.asChild (Base UI) renders as a different element instead of wrapping.aria-label. Icons are lucide-react glyphs, sized with Tailwind (size-*) and coloured by currentColor.@aira/react and charts from @aira/react/charts.--aira-*-prefixed (antd v3 coexistence). Numeric/currency formatting is dynamic per brand locale (never a hard-coded prefix).date-fns — no hand-rolled date math.AiraListRail), the change-log feed, dropdown options, data tables — is virtualized with @tanstack/react-virtual.:focus-visible ring (no per-component override).aria-* wiring comes from @base-ui/react primitives — don't re-implement roles by hand.System-wide status. Figma means a 1:1 component spec exists in the Figma bundle — the Figma file itself isn't built yet. HTML means a live reference renders in this file. A11y means keyboard, ARIA, and contrast were checked in light and dark.
| Component | Figma | HTML | A11y | Status |
|---|---|---|---|---|
| Button | ✓ | ✓ | ✓ | |
| Icon button | ✓ | ✓ | ✓ | |
| Input | ✓ | ✓ | ✓ | |
| Textarea | ✓ | ✓ | ✓ | |
| Chip | ✓ | ✓ | ✓ | |
| Tag | ✓ | ✓ | ✓ | |
| Badge | ✓ | ✓ | ✓ | |
| Pill (status) | ✓ | ✓ | ✓ | |
| Avatar | ✓ | ✓ | ✓ | |
| Citation chip | ✓ | ✓ | ✓ | |
| Tooltip | ✓ | ✓ | ✓ | |
| Keyboard (kbd) | ✓ | ✓ | ✓ | |
| Divider | ✓ | ✓ | ✓ | |
| Spinner | ✓ | ✓ | ✓ | |
| Skeleton | ✓ | ✓ | ✓ | |
| Field | ✓ | ✓ | ✓ | |
| Dropdown | ✓ | ✓ | ✓ | |
| Checkbox | ✓ | ✓ | ✓ | |
| Radio | ✓ | ✓ | ✓ | |
| Toggle | ✓ | ✓ | ✓ | |
| Stepper | ✓ | ✓ | ✓ | |
| Progress bar | ✓ | ✓ | ✓ | |
| Feedback control | ✓ | ✓ | ✓ | |
| Action row | ✓ | ✓ | ✓ | |
| Tabs | ✓ | ✓ | ✓ | |
| Breadcrumb | ✓ | ✓ | ✓ | |
| Popover | ✓ | ✓ | ✓ | |
| Toast | ✓ | ✓ | ✓ | |
| Banner | ✓ | ✓ | ✓ | |
| Nudge card | ✓ | ✓ | ✓ | |
| Selection card | ✓ | ✓ | ✓ | |
| To-do list | ✓ | ✓ | Partial | |
| Chat message | ✓ | ✓ | ✓ | |
| Composer | ✓ | ✓ | ✓ | |
| Approval card | ✓ | ✓ | ✓ | |
| Code block | ✓ | ✓ | ✓ | |
| Inline diff | ✓ | ✓ | ✓ | |
| Agent activity | ✓ | ✓ | ✓ | |
| Conversation state machine | ✓ | ✓ | Partial | |
| Modal / Dialog | ✓ | ✓ | ✓ | |
| Bottom sheet | ✓ | ✓ | ✓ | |
| List row | ✓ | ✓ | ✓ | |
| Data table | ✓ | ✓ | ✓ | |
| Card | ✓ | ✓ | ✓ | |
| Form | ✓ | ✓ | ✓ | |
| Change-log feed | ✓ | ✓ | Partial |
The Figma library mirrors the atomic IA. Tokens published as Figma variables from tokens.json in DTCG 2025.10 format.
{
"$themes": [
{
"id": "light",
"name": "Light",
"selectedTokenSets": {
"global": "enabled",
"primitives": "source",
"semantic-light": "enabled",
"additions-light": "enabled"
}
},
{
"id": "dark",
"name": "Dark",
"selectedTokenSets": {
"global": "enabled",
"primitives": "source",
"semantic-dark": "enabled",
"additions-dark": "enabled"
}
}
],
"$metadata": {
"tokenSetOrder": [
"primitives",
"semantic-light",
"semantic-dark",
"additions-light",
"additions-dark",
"global"
]
},
"primitives": {
"slate": {
"25": {
"$value": "#FBFCFD",
"$type": "color"
},
"50": {
"$value": "#F6F8FA",
"$type": "color"
},
"100": {
"$value": "#EEF1F4",
"$type": "color"
},
"200": {
"$value": "#E2E6EB",
"$type": "color"
},
"300": {
"$value": "#C9D0D9",
"$type": "color"
},
"400": {
"$value": "#9AA4B2",
"$type": "color"
},
"500": {
"$value": "#6B7785",
"$type": "color"
},
"600": {
"$value": "#4D5867",
"$type": "color"
},
"700": {
"$value": "#364152",
"$type": "color"
},
"800": {
"$value": "#1F2937",
"$type": "color"
},
"900": {
"$value": "#111827",
"$type": "color"
},
"950": {
"$value": "#0A0F1A",
"$type": "color"
}
},
"indigo": {
"50": {
"$value": "#EEF0FF",
"$type": "color"
},
"100": {
"$value": "#DDE0FF",
"$type": "color"
},
"200": {
"$value": "#BAC1FF",
"$type": "color"
},
"300": {
"$value": "#8E97FF",
"$type": "color"
},
"400": {
"$value": "#6268FF",
"$type": "color"
},
"500": {
"$value": "#4A50EF",
"$type": "color"
},
"600": {
"$value": "#3438C9",
"$type": "color"
},
"700": {
"$value": "#252AA0",
"$type": "color"
},
"800": {
"$value": "#1B2078",
"$type": "color"
},
"900": {
"$value": "#121757",
"$type": "color"
}
},
"violet": {
"50": {
"$value": "#F4EEFF",
"$type": "color"
},
"100": {
"$value": "#E6DAFF",
"$type": "color"
},
"200": {
"$value": "#CDB5FF",
"$type": "color"
},
"300": {
"$value": "#B08CFF",
"$type": "color"
},
"400": {
"$value": "#8E61F2",
"$type": "color"
},
"500": {
"$value": "#6E40D9",
"$type": "color"
},
"600": {
"$value": "#5A2FB8",
"$type": "color"
},
"700": {
"$value": "#46248F",
"$type": "color"
}
},
"sky": {
"50": {
"$value": "#E6F1FF",
"$type": "color"
},
"100": {
"$value": "#C7E1FF",
"$type": "color"
},
"200": {
"$value": "#8EC1FF",
"$type": "color"
},
"300": {
"$value": "#5EA8FF",
"$type": "color"
},
"400": {
"$value": "#3BA7FF",
"$type": "color"
},
"500": {
"$value": "#1564CC",
"$type": "color"
},
"600": {
"$value": "#114DA0",
"$type": "color"
},
"700": {
"$value": "#0D3C7E",
"$type": "color"
}
},
"green": {
"50": {
"$value": "#E7F6EC",
"$type": "color"
},
"100": {
"$value": "#C6EAD2",
"$type": "color"
},
"200": {
"$value": "#8FD7A6",
"$type": "color"
},
"300": {
"$value": "#4FBE76",
"$type": "color"
},
"400": {
"$value": "#2A9D55",
"$type": "color"
},
"500": {
"$value": "#1F7A42",
"$type": "color"
},
"600": {
"$value": "#185C32",
"$type": "color"
},
"700": {
"$value": "#103E22",
"$type": "color"
}
},
"amber": {
"50": {
"$value": "#FFF6E0",
"$type": "color"
},
"100": {
"$value": "#FFE5A8",
"$type": "color"
},
"200": {
"$value": "#FFCB60",
"$type": "color"
},
"300": {
"$value": "#E89B1A",
"$type": "color"
},
"400": {
"$value": "#B97300",
"$type": "color"
},
"500": {
"$value": "#8A5500",
"$type": "color"
},
"600": {
"$value": "#5C3800",
"$type": "color"
}
},
"red": {
"50": {
"$value": "#FFECEC",
"$type": "color"
},
"100": {
"$value": "#FFCFCF",
"$type": "color"
},
"200": {
"$value": "#FF9C9C",
"$type": "color"
},
"300": {
"$value": "#F55959",
"$type": "color"
},
"400": {
"$value": "#D92D2D",
"$type": "color"
},
"500": {
"$value": "#B11E1E",
"$type": "color"
},
"600": {
"$value": "#8A1414",
"$type": "color"
}
}
},
"semantic-light": {
"bg": {
"canvas": {
"$value": "{primitives.slate.50}",
"$type": "color"
},
"surface": {
"$value": "#FFFFFF",
"$type": "color"
},
"surfaceAlt": {
"$value": "{primitives.slate.25}",
"$type": "color"
},
"sunken": {
"$value": "{primitives.slate.100}",
"$type": "color"
},
"brand": {
"$value": "{primitives.indigo.600}",
"$type": "color"
},
"brandSubtle": {
"$value": "{primitives.indigo.50}",
"$type": "color"
},
"selected": {
"$value": "{primitives.indigo.50}",
"$type": "color"
}
},
"text": {
"primary": {
"$value": "{primitives.slate.900}",
"$type": "color"
},
"secondary": {
"$value": "{primitives.slate.700}",
"$type": "color"
},
"tertiary": {
"$value": "{primitives.slate.500}",
"$type": "color"
},
"onBrand": {
"$value": "#FFFFFF",
"$type": "color"
},
"link": {
"$value": "{primitives.indigo.600}",
"$type": "color"
}
},
"border": {
"subtle": {
"$value": "{primitives.slate.100}",
"$type": "color"
},
"default": {
"$value": "{primitives.slate.200}",
"$type": "color"
},
"strong": {
"$value": "{primitives.slate.300}",
"$type": "color"
},
"focus": {
"$value": "{primitives.indigo.600}",
"$type": "color"
},
"danger": {
"$value": "{primitives.red.400}",
"$type": "color"
}
}
},
"semantic-dark": {
"bg": {
"canvas": {
"$value": "#0E1424",
"$type": "color"
},
"surface": {
"$value": "#131B2C",
"$type": "color"
},
"surfaceAlt": {
"$value": "#182135",
"$type": "color"
},
"sunken": {
"$value": "#0A101C",
"$type": "color"
},
"brand": {
"$value": "{primitives.indigo.400}",
"$type": "color"
},
"brandSubtle": {
"$value": "#1F2A55",
"$type": "color"
},
"selected": {
"$value": "#1F2A55",
"$type": "color"
}
},
"text": {
"primary": {
"$value": "#F4F6FA",
"$type": "color"
},
"secondary": {
"$value": "#C8CFDB",
"$type": "color"
},
"tertiary": {
"$value": "#94A0B4",
"$type": "color"
},
"onBrand": {
"$value": "#0A0F1A",
"$type": "color"
},
"link": {
"$value": "{primitives.indigo.300}",
"$type": "color"
}
},
"border": {
"subtle": {
"$value": "#1F2940",
"$type": "color"
},
"default": {
"$value": "#2A3654",
"$type": "color"
},
"strong": {
"$value": "#3A4870",
"$type": "color"
},
"focus": {
"$value": "{primitives.indigo.300}",
"$type": "color"
},
"danger": {
"$value": "{primitives.red.300}",
"$type": "color"
}
}
},
"global": {
"type": {
"displayLg": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "40px",
"lineHeight": "48px",
"fontWeight": "600",
"letterSpacing": "-0.02em"
},
"$type": "typography"
},
"displayMd": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "32px",
"lineHeight": "40px",
"fontWeight": "600",
"letterSpacing": "-0.015em"
},
"$type": "typography"
},
"h1": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "28px",
"lineHeight": "36px",
"fontWeight": "600",
"letterSpacing": "-0.01em"
},
"$type": "typography"
},
"h2": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "22px",
"lineHeight": "30px",
"fontWeight": "600",
"letterSpacing": "-0.005em"
},
"$type": "typography"
},
"h3": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "18px",
"lineHeight": "26px",
"fontWeight": "600"
},
"$type": "typography"
},
"h4": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "16px",
"lineHeight": "24px",
"fontWeight": "600"
},
"$type": "typography"
},
"bodyLg": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "16px",
"lineHeight": "26px",
"fontWeight": "400"
},
"$type": "typography"
},
"bodyMd": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "14px",
"lineHeight": "22px",
"fontWeight": "400"
},
"$type": "typography"
},
"bodySm": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "13px",
"lineHeight": "20px",
"fontWeight": "400"
},
"$type": "typography"
},
"labelLg": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "15px",
"lineHeight": "22px",
"fontWeight": "500"
},
"$type": "typography"
},
"labelMd": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "14px",
"lineHeight": "20px",
"fontWeight": "500"
},
"$type": "typography"
},
"labelSm": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "12px",
"lineHeight": "16px",
"fontWeight": "500"
},
"$type": "typography"
},
"caption": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "12px",
"lineHeight": "16px",
"fontWeight": "400"
},
"$type": "typography"
},
"overline": {
"$value": {
"fontFamily": "Roboto",
"fontSize": "11px",
"lineHeight": "16px",
"fontWeight": "500",
"letterSpacing": "0.08em"
},
"$type": "typography"
},
"code": {
"$value": {
"fontFamily": "SF Mono",
"fontSize": "13px",
"lineHeight": "20px",
"fontWeight": "400"
},
"$type": "typography"
}
},
"spacing": {
"0": {
"$value": "0px",
"$type": "dimension"
},
"1": {
"$value": "4px",
"$type": "dimension"
},
"2": {
"$value": "8px",
"$type": "dimension"
},
"3": {
"$value": "12px",
"$type": "dimension"
},
"4": {
"$value": "16px",
"$type": "dimension"
},
"5": {
"$value": "20px",
"$type": "dimension"
},
"6": {
"$value": "24px",
"$type": "dimension"
},
"8": {
"$value": "32px",
"$type": "dimension"
},
"10": {
"$value": "40px",
"$type": "dimension"
},
"12": {
"$value": "48px",
"$type": "dimension"
},
"16": {
"$value": "64px",
"$type": "dimension"
},
"20": {
"$value": "80px",
"$type": "dimension"
}
},
"radius": {
"xs": {
"$value": "4px",
"$type": "dimension"
},
"sm": {
"$value": "6px",
"$type": "dimension"
},
"md": {
"$value": "8px",
"$type": "dimension"
},
"lg": {
"$value": "12px",
"$type": "dimension"
},
"xl": {
"$value": "16px",
"$type": "dimension"
},
"2xl": {
"$value": "20px",
"$type": "dimension"
},
"pill": {
"$value": "999px",
"$type": "dimension"
},
"circle": {
"$value": "9999px",
"$type": "dimension"
}
},
"elevation": {
"xs": {
"$value": {
"color": "rgba(17,24,39,0.06)",
"offsetX": "0px",
"offsetY": "1px",
"blur": "2px",
"spread": "0px"
},
"$type": "shadow"
},
"sm": {
"$value": {
"color": "rgba(17,24,39,0.08)",
"offsetX": "0px",
"offsetY": "1px",
"blur": "3px",
"spread": "0px"
},
"$type": "shadow"
},
"md": {
"$value": {
"color": "rgba(17,24,39,0.10)",
"offsetX": "0px",
"offsetY": "4px",
"blur": "8px",
"spread": "-2px"
},
"$type": "shadow"
},
"lg": {
"$value": {
"color": "rgba(17,24,39,0.14)",
"offsetX": "0px",
"offsetY": "12px",
"blur": "24px",
"spread": "-6px"
},
"$type": "shadow"
},
"focus": {
"$value": {
"color": "rgba(52,56,201,0.32)",
"offsetX": "0px",
"offsetY": "0px",
"blur": "0px",
"spread": "3px"
},
"$type": "shadow"
}
},
"motion": {
"fast": {
"$value": "120ms cubic-bezier(0.4,0,0.2,1)",
"$type": "duration"
},
"base": {
"$value": "200ms cubic-bezier(0.4,0,0.2,1)",
"$type": "duration"
},
"slow": {
"$value": "320ms cubic-bezier(0.4,0,0.2,1)",
"$type": "duration"
}
}
},
"additions-light": {
"ai": {
"accent": {
"$value": "{primitives.violet.600}",
"$type": "color"
},
"accentSurface": {
"$value": "{primitives.violet.50}",
"$type": "color"
},
"accentBorder": {
"$value": "{primitives.violet.200}",
"$type": "color"
},
"onAccent": {
"$value": "#FFFFFF",
"$type": "color"
}
},
"citation": {
"bg": {
"$value": "{primitives.slate.50}",
"$type": "color"
},
"bgHover": {
"$value": "{primitives.slate.100}",
"$type": "color"
},
"border": {
"$value": "{primitives.slate.200}",
"$type": "color"
},
"text": {
"$value": "{primitives.slate.700}",
"$type": "color"
},
"icon": {
"$value": "{primitives.slate.500}",
"$type": "color"
}
},
"diff": {
"addBg": {
"$value": "#E7F6EC",
"$type": "color"
},
"addFg": {
"$value": "{primitives.green.600}",
"$type": "color"
},
"addMarker": {
"$value": "{primitives.green.400}",
"$type": "color"
},
"removeBg": {
"$value": "#FFECEC",
"$type": "color"
},
"removeFg": {
"$value": "{primitives.red.500}",
"$type": "color"
},
"removeMarker": {
"$value": "{primitives.red.400}",
"$type": "color"
},
"contextBg": {
"$value": "#FFFFFF",
"$type": "color"
},
"contextFg": {
"$value": "{primitives.slate.700}",
"$type": "color"
},
"gutterBg": {
"$value": "{primitives.slate.50}",
"$type": "color"
},
"gutterFg": {
"$value": "{primitives.slate.500}",
"$type": "color"
}
},
"code": {
"bg": {
"$value": "{primitives.slate.50}",
"$type": "color"
},
"bgInline": {
"$value": "{primitives.slate.100}",
"$type": "color"
},
"border": {
"$value": "{primitives.slate.200}",
"$type": "color"
},
"fg": {
"$value": "{primitives.slate.900}",
"$type": "color"
}
},
"syntax": {
"keyword": {
"$value": "{primitives.indigo.700}",
"$type": "color"
},
"string": {
"$value": "{primitives.green.600}",
"$type": "color"
},
"number": {
"$value": "{primitives.amber.500}",
"$type": "color"
},
"comment": {
"$value": "{primitives.slate.500}",
"$type": "color"
},
"operator": {
"$value": "{primitives.slate.700}",
"$type": "color"
},
"function": {
"$value": "{primitives.violet.600}",
"$type": "color"
},
"punctuation": {
"$value": "{primitives.slate.600}",
"$type": "color"
}
},
"approval": {
"draftBg": {
"$value": "{primitives.slate.100}",
"$type": "color"
},
"draftText": {
"$value": "{primitives.slate.700}",
"$type": "color"
},
"awaitingBg": {
"$value": "{primitives.amber.50}",
"$type": "color"
},
"awaitingText": {
"$value": "{primitives.amber.500}",
"$type": "color"
},
"approvedBg": {
"$value": "{primitives.green.50}",
"$type": "color"
},
"approvedText": {
"$value": "{primitives.green.600}",
"$type": "color"
},
"liveBg": {
"$value": "{primitives.indigo.50}",
"$type": "color"
},
"liveText": {
"$value": "{primitives.indigo.700}",
"$type": "color"
},
"rejectedBg": {
"$value": "{primitives.red.50}",
"$type": "color"
},
"rejectedText": {
"$value": "{primitives.red.500}",
"$type": "color"
}
}
},
"additions-dark": {
"ai": {
"accent": {
"$value": "{primitives.violet.200}",
"$type": "color"
},
"accentSurface": {
"$value": "#2A1A4D",
"$type": "color"
},
"accentBorder": {
"$value": "{primitives.violet.400}",
"$type": "color"
},
"onAccent": {
"$value": "#0A0F1A",
"$type": "color"
}
},
"citation": {
"bg": {
"$value": "#182135",
"$type": "color"
},
"bgHover": {
"$value": "#1C2640",
"$type": "color"
},
"border": {
"$value": "#2A3654",
"$type": "color"
},
"text": {
"$value": "#C8CFDB",
"$type": "color"
},
"icon": {
"$value": "{primitives.slate.400}",
"$type": "color"
}
},
"diff": {
"addBg": {
"$value": "#0E2E1B",
"$type": "color"
},
"addFg": {
"$value": "#6FD995",
"$type": "color"
},
"addMarker": {
"$value": "{primitives.green.300}",
"$type": "color"
},
"removeBg": {
"$value": "#2E1010",
"$type": "color"
},
"removeFg": {
"$value": "#F58B8B",
"$type": "color"
},
"removeMarker": {
"$value": "{primitives.red.300}",
"$type": "color"
},
"contextBg": {
"$value": "#131B2C",
"$type": "color"
},
"contextFg": {
"$value": "#C8CFDB",
"$type": "color"
},
"gutterBg": {
"$value": "#0A101C",
"$type": "color"
},
"gutterFg": {
"$value": "{primitives.slate.400}",
"$type": "color"
}
},
"code": {
"bg": {
"$value": "#0A101C",
"$type": "color"
},
"bgInline": {
"$value": "#182135",
"$type": "color"
},
"border": {
"$value": "#2A3654",
"$type": "color"
},
"fg": {
"$value": "#F4F6FA",
"$type": "color"
}
},
"syntax": {
"keyword": {
"$value": "{primitives.indigo.200}",
"$type": "color"
},
"string": {
"$value": "#6FD995",
"$type": "color"
},
"number": {
"$value": "#F0B95B",
"$type": "color"
},
"comment": {
"$value": "{primitives.slate.400}",
"$type": "color"
},
"operator": {
"$value": "#C8CFDB",
"$type": "color"
},
"function": {
"$value": "{primitives.violet.200}",
"$type": "color"
},
"punctuation": {
"$value": "{primitives.slate.300}",
"$type": "color"
}
},
"approval": {
"draftBg": {
"$value": "#1C2640",
"$type": "color"
},
"draftText": {
"$value": "#C8CFDB",
"$type": "color"
},
"awaitingBg": {
"$value": "#2E2310",
"$type": "color"
},
"awaitingText": {
"$value": "#F0B95B",
"$type": "color"
},
"approvedBg": {
"$value": "#0E2E1B",
"$type": "color"
},
"approvedText": {
"$value": "#6FD995",
"$type": "color"
},
"liveBg": {
"$value": "#1F2A55",
"$type": "color"
},
"liveText": {
"$value": "#BCC4FF",
"$type": "color"
},
"rejectedBg": {
"$value": "#2E1010",
"$type": "color"
},
"rejectedText": {
"$value": "#F58B8B",
"$type": "color"
}
}
},
"$schema": "https://design-tokens.github.io/community-group/format/2025-10/",
"$description": "Aira Design System v1.1 \u2014 DTCG 2025.10 format. Agent identity tokens removed (rule: one aiRA)."
}
Note: a Figma file has not been created for these components yet. This spec documents the intended structure — variants, properties, and naming — ahead of the Figma build, so the eventual library matches the code 1:1.
This file is the source of truth a Figma designer will use to draw the v1.0 component frames. Component names below match CSS selectors in ../index.html 1:1, so Figma → code stays in sync.
How to use 1. Import tokens.json via the Tokens Studio plugin → tokens become Figma variables. 2. For each component below, create a Figma Component with the listed variants and properties. 3. Name the Figma component exactly as the Component name field — this matches code-connect.json. 4. Reference the live HTML demo in ../index.html for visual sizing.
All measurements in px. All colors reference Tokens Studio variables (e.g. ai.accent).
---
---
Component name: chat/Bubble Selector: .msg
Variants (Figma variant property):
role: user · ai · system · erroragent (ai only): config · qa · analytics · support · coderstate: default · hover · selecteddensity: comfortable · compactProperties (Figma component properties):
Text → message body (instance swap, supports markdown)Show meta → boolean (default true for ai/error)Show actions → booleanTimestamp → textAnatomy (top → bottom): 1. Meta row (ai only) — Auto-layout horizontal, 8px gap, 4px bottom padding. - AgentBadge slot · AILabel · Timestamp (caption, tertiary) 2. Body — Max width 80%, padding 12/16, radius lg (12), one corner pinched to xs (4). - user: bg brand, text onBrand, right-pinch - ai: bg surface, text primary, border default, left-pinch - system: bg statusNeutral, text secondary, full radius md - error: bg state.aiError, border state.aiError, text state.aiError 3. Action row (optional) — 8px top gap, horizontal auto-layout. Copy · Support · Feedback group.
States | State | Treatment | |---|---| | default | base | | hover | actions reveal (opacity 0 → 1, 120ms) | | focus-visible | 2px ring border.focus, offset 2 | | streaming (ai) | trailing pulse dot, 1.2s ease loop |
A11y: list wrapper has aria-live="polite". Action buttons have aria-label. AI label is text + dot, never color only.
---
Component name: ai/Label Selector: .ai-label
Variants:
size: inline (pill, 11/16 overline) · block (label, 12/16, full disclosure copy)Anatomy:
ai.accent, 6px gap.ai.accent on ai.accentSurface, border ai.accentBorder.pill for inline, md for block.Mandatory use: on every AI-authored output (chat replies, generated configs, generated SQL, drafted copy).
---
Component name: approval/Card Selector: .approval
Variants:
status: draft · awaiting · approved · live · rejectedrisk: standard · destructive (drives primary button variant)Properties:
Title (header text)Agent (AgentBadge swap)Preview slot (instance swap: Diff, Table, Chart, RichText)Anatomy (vertical auto-layout, radius lg, border ai.accentBorder): 1. Header — Padding 16/20, bg ai.accentSurface, border-bottom subtle. - AgentBadge · AILabel ("Proposal") · Title (h4, primary) 2. Body — Padding 16/20. - Framing line (body-md, secondary) - Preview slot 3. Footer — Padding 12/20, bg surfaceAlt, border-top subtle. - Status pill (left) · Spacer · Edit (secondary) · Create (primary, right)
States | Status | Pill | Primary action | |---|---|---| | draft | Draft (slate) | Submit for review | | awaiting | Awaiting approval (amber) | Approve / Reject | | approved | Approved (green) | Make live | | live | Live (indigo) | (read-only) | | rejected | Rejected (red) | Revert to draft |
A11y: wrap as <article aria-labelledby>. For destructive risk, chain a Modal confirmation.
---
Component name: citation/Chip Selector: .citation
Variants:
mode: inline · listedstate: default · hover · focusProperties:
Number (overline)Title (label-sm)Show snippet on hover (boolean — pairs with Popover)Anatomy:
sm.citation.bg, text citation.text, border citation.border.Sources block under the answer.A11y: real anchor (<a href>) — never div onclick. Hover popover surfaces the cited snippet.
---
Component name: code/Block Selector: .code-block
Variants:
lang: sql · javascript · python · yaml · textstate: default · collapsedai-labelled: true · falseProperties:
Code (text, monospace)Lang (badge text)Show Copy (boolean)Anatomy:
md, border code.border, bg code.bg.surfaceAlt, border-bottom. Lang overline (tertiary) · AILabel (optional) · spacer · Copy (tertiary button).code, color code.fg. Syntax token colors per v2-additions..syntax..Collapsed: cap at 240px height with a "Show all" tertiary button below.
---
Component name: diff/Inline Selector: .diff
Variants:
density: comfortable · compactshow-line-numbers: true · falseAnatomy (per line: 36px gutter + 1fr body):
diff.gutterBg, fg diff.gutterFg, font overline, right-aligned.diff.addBg, fg diff.addFg, gutter prefixed with +.diff.removeBg, fg diff.removeFg, gutter prefixed with −.diff.contextBg, fg diff.contextFg.Always pair sign + color — the + / − marker is the primary signal.
---
Component name: activity/Row and activity/List Selector: .activity, .activity-list
Variants (Row):
state: thinking · streaming · stopped · errorVariants (List item):
state: pending · active · done · failedAnatomy (Row):
md, bg state.aiThinking, border state.aiThinking, text state.aiThinking.Anatomy (List):
A11y: wrap in role="status" aria-live="polite". Honor prefers-reduced-motion (spinner stops, surface stays).
---
Component name: feedback/Control Selector: .feedback
Variants:
state: default · up-selected · down-selected · disabledsize: sm (24) · md (28) · lg (32)Anatomy:
sm, border default.statusSuccess, border green.300.statusDanger, border red.300.A11y: role="group" + aria-label="Was this helpful?". Selected button uses aria-pressed="true". Down selection opens a reason picker Popover.
---
Component name: overlay/Modal Selector: .modal
Variants:
intent: confirmation · destructive · info · formsize: sm (440) · md (560) · lg (720)Properties:
Title (h3, primary)Body slot (instance swap)Primary CTA (button)Secondary CTA (button)Anatomy:
xl (16). Shadow lg.subtle.surfaceAlt, border-top, actions right-aligned with 8px gap.Scrim: bg.overlay (rgba slate-900 .48 light / black .60 dark).
A11y: role="dialog" aria-modal="true" aria-labelledby. Focus trap. Esc closes. Focus returns to trigger.
---
Component name: overlay/BottomSheet Selector: .sheet
Variants:
size: peek (25%) · default (50%) · full (95%)dismissible: true · false (drag handle visibility & scrim click)Anatomy:
lg. Bg surface.20 + safe.bottom.border.strong, centered, 12px below the top edge.A11y: same as Modal. Handle is decorative — always include explicit Cancel / Apply buttons.
---
---
Component name: nav/Tabs Selector: .tabs
Variants: size: sm · md. style: line · contained. Anatomy: horizontal auto-layout, border-bottom default. Each tab: padding 12/12, 2px bottom border (transparent → border.focus when selected). Selected: text link. A11y: role="tablist", items role="tab" with aria-selected. Arrow-key nav.
---
Component name: form/Select Selector: .field select
Variants: state: default · hover · focus · open · filled · disabled · error. Anatomy: 36px height, padding 8/12 (right 32 for chevron). Bg surface, border default, radius md. Chevron: two 5px slate-tertiary stripes.
---
Component name: form/Combobox Selector: .combo
Variants: state: closed · open · loading · empty. Anatomy:
surface, border default, radius md, shadow md, max-height 240, scrollable.hover, selected bg selected. Optional sub-label (caption, right-aligned, tertiary).A11y: role="combobox" + aria-expanded + aria-controls. Menu role="listbox". Items role="option" + aria-selected.
---
Component name: form/Toggle Selector: .toggle
Variants: state: off · on · disabled · focused. Anatomy: Track 36×20, radius 999. Thumb 16×16, white, shadow xs. Off: bg slate.300. On: bg brand. Translate 16 on toggle, 200ms ease.
---
Component name: form/Checkbox Selector: .opt input[type=checkbox]
Variants: state: unchecked · checked · indeterminate · disabled · error. Anatomy: 18×18, radius xs. Unchecked: border default. Checked: bg brand, white tick. Indeterminate: bg brand, white horizontal bar.
---
Component name: form/RadioGroup Selector: fieldset + .opt input[type=radio]
Variants (per option): state: unchecked · checked · disabled. Anatomy: 18×18 circle, radius 50%. Checked: 6px inner dot in brand. A11y: wrap in <fieldset> with <legend>. Arrow keys move between options.
---
Component name: overlay/Tooltip, overlay/Popover Selector: .tip, .popover
Tooltip:
slate.900 (light) / slate.100 (dark). Text white/slate-900. Padding 6/8. Radius sm. Shadow md. Max-width 220.Popover:
surface, border default, radius md, shadow md, padding 12/16. Max-width 320.role="dialog").---
Component name: avatar/Avatar Selector: .avatar
Variants:
size: sm (24) · md (32) · lg (44)kind: user · ai · agent.config · agent.qa · agent.analytics · agent.support · agent.coderAnatomy: circle, font label-md (sm: label-sm, lg: h4). user: bg brand, text onBrand. ai: gradient aira-gradient. agent.: bg matching agent. token.
---
Component name: list/Row Selector: .list-row
Variants:
density: comfortable (min-height 44) · dense (min-height 36)state: default · hover · selected · disabledAnatomy: 3-column grid (auto · 1fr · auto), 12px gap, padding 12/16. Border-bottom subtle.
A11y: entire row is one interactive surface (<a> or <button>); never split.
---
Component name: data/Table Selector: .dt
Variants (cell): align: start · end. type: text · number · delta. Anatomy:
surfaceAlt, label-sm, text primary.tableDivider. Hover bg hover.delta.positive (green) with +; negative uses delta.negative (orange, NOT red) with −.---
Component names: progress/Stepper, progress/Bar Selectors: .stepper, .progress
Stepper variants (per step): state: pending · active · done · failed. Anatomy:
Progress bar:
sunken. Fill brand, animated to width.role="progressbar" + aria-valuemin/max/now.---
Skeleton — loading/Skeleton. Selector .skeleton. Bg = animated gradient sunken → hover → sunken, 1.4s loop. aria-hidden="true". Variants: line · circle · block · bubble.
Form anatomy — composition pattern, not a component. Wrap fields in .field; label connected via for/id; helper text (caption) below; error text (caption, danger) replaces helper on aria-invalid="true"; required marker = visible asterisk + required attribute.
---
All Figma components are named category/Name (Pascal-cased name), e.g. chat/Bubble, overlay/Modal. The slash creates Figma's nested-component menu structure. The code-connect.json file pairs each name to its file path so Figma's Dev Mode can jump to source.
{
"$schema": "https://schemas.figma.com/code-connect/v1/code-connect.schema.json",
"version": "1.0.0",
"designSystem": "Aira Design System v1.0",
"sourceRoot": "../",
"components": [
{
"figmaName": "chat/Bubble",
"source": "index.html#chat-bubble",
"selector": ".msg",
"variants": ["role", "agent", "state", "density"],
"props": ["text", "showMeta", "showActions", "timestamp"]
},
{
"figmaName": "ai/Label",
"source": "index.html#ai-label",
"selector": ".ai-label",
"variants": ["size"]
},
{
"figmaName": "approval/Card",
"source": "index.html#approval-card",
"selector": ".approval",
"variants": ["status", "risk"],
"props": ["title", "agent", "previewSlot"]
},
{
"figmaName": "citation/Chip",
"source": "index.html#citation",
"selector": ".citation",
"variants": ["mode", "state"],
"props": ["number", "title", "showSnippetOnHover"]
},
{
"figmaName": "code/Block",
"source": "index.html#code-block",
"selector": ".code-block",
"variants": ["lang", "state", "aiLabelled"],
"props": ["code", "lang", "showCopy"]
},
{
"figmaName": "diff/Inline",
"source": "index.html#diff",
"selector": ".diff",
"variants": ["density", "showLineNumbers"]
},
{
"figmaName": "activity/Row",
"source": "index.html#activity",
"selector": ".activity",
"variants": ["state"]
},
{
"figmaName": "activity/List",
"source": "index.html#activity",
"selector": ".activity-list",
"variants": ["itemState"]
},
{
"figmaName": "feedback/Control",
"source": "index.html#feedback",
"selector": ".feedback",
"variants": ["state", "size"]
},
{
"figmaName": "overlay/Modal",
"source": "index.html#modal",
"selector": ".modal",
"variants": ["intent", "size"],
"props": ["title", "bodySlot", "primaryCta", "secondaryCta"]
},
{
"figmaName": "overlay/BottomSheet",
"source": "index.html#bottom-sheet",
"selector": ".sheet",
"variants": ["size", "dismissible"]
},
{
"figmaName": "nav/Tabs",
"source": "index.html#tabs",
"selector": ".tabs",
"variants": ["size", "style"]
},
{
"figmaName": "form/Select",
"source": "index.html#select",
"selector": ".field select",
"variants": ["state"]
},
{
"figmaName": "form/Combobox",
"source": "index.html#combobox",
"selector": ".combo",
"variants": ["state"]
},
{
"figmaName": "form/Toggle",
"source": "index.html#toggle",
"selector": ".toggle",
"variants": ["state"]
},
{
"figmaName": "form/Checkbox",
"source": "index.html#checkbox",
"selector": ".opt input[type=checkbox]",
"variants": ["state"]
},
{
"figmaName": "form/RadioGroup",
"source": "index.html#radio",
"selector": "fieldset",
"variants": ["optionState"]
},
{
"figmaName": "overlay/Tooltip",
"source": "index.html#tooltip",
"selector": ".tip"
},
{
"figmaName": "overlay/Popover",
"source": "index.html#tooltip",
"selector": ".popover"
},
{
"figmaName": "avatar/Avatar",
"source": "index.html#avatar",
"selector": ".avatar",
"variants": ["size", "kind"]
},
{
"figmaName": "list/Row",
"source": "index.html#list-row",
"selector": ".list-row",
"variants": ["density", "state"]
},
{
"figmaName": "data/Table",
"source": "index.html#data-table",
"selector": ".dt",
"variants": ["cellAlign", "cellType"]
},
{
"figmaName": "progress/Stepper",
"source": "index.html#stepper",
"selector": ".stepper",
"variants": ["stepState"]
},
{
"figmaName": "progress/Bar",
"source": "index.html#stepper",
"selector": ".progress"
},
{
"figmaName": "loading/Skeleton",
"source": "index.html#skeleton",
"selector": ".skeleton",
"variants": ["shape"]
}
],
"tokens": {
"format": "tokens-studio",
"source": "tokens.json",
"css": "../tokens.css"
},
"notes": [
"Import tokens.json via the Tokens Studio Figma plugin to materialize variables/styles.",
"Each figmaName above must be created as a Figma Component with the listed variants.",
"After creating the components, run Figma → Plugins → Code Connect → Sync to bind them to this file.",
"The figmaName slashes ('chat/Bubble') create Figma's nested-component menu structure."
]
}
Work consciously left out of Aira Design System v1.1. Each item has a trigger condition for re-opening.
| Item | Why deferred | Re-open when |
|---|---|---|
| Cmd+K command palette + slash commands | The command surface isn't designed yet (which commands, namespaced how, scoped to which agent). | Product defines the command vocabulary. |
| Multi-brand theming hooks | The reseller/white-label contract isn't agreed yet. | First reseller deployment commits. |
| State-layer system (opacity-based hover/pressed) | Token cost and complexity outweighs the benefit at the current system scale. | System grows past ~80 components. |
| Density modes (compact / comfortable / spacious) | Two density requirements would force every component to re-spec. Premature. | A product asks for compact view in writing. |
| Responsive breakpoint tokens | The v1.1 surfaces are desktop-first; mobile is a single break. | Tablet or kiosk surface ships. |
| Style Dictionary export (JS / Swift / Kotlin) | Only the web app consumes tokens today. | Native client (iOS or Android) commits to the system. |
| i18n & bidi foundation pages | The earlier standalone pages were dropped; the rule lives in tokens.css §7a but isn't documented. | Next edition. |
@base-ui/react APIs may still shift; pin the version and isolate changes behind the AiraX wrappers.recharts (SVG) degrades past a few thousand points; drop to a d3 + canvas renderer for high-cardinality series.@tanstack/react-virtual removes off-screen rows from the DOM; keep aria-rowcount/aria-setsize accurate so assistive tech still reads totals.value + onValueChange, not bare onChange handlers.aria-label fails lint.new Date() math or hand-built format strings in components.@tanstack/react-virtual fails lint.date-fns locales; currency/grouping resolve from the brand locale (never a hard-coded prefix).dir + CSS logical properties (the system already ships LTR/RTL); no left/right hard-coding.Version history for the Aira Design System itself — tokens, components, voice rules, documentation. Every entry is dated and tagged so you can tell at a glance whether something was added, changed, removed, or fixed between releases. For the in-product change-log pattern (audit trails, edit timelines), see Change-log feed.
bg-canvas (#F6F8FA / #0E1424), artifact panel = bg-surface-alt (#FBFCFD / #182135), cards / top bar / sidebar / rail = bg-surface (#FFFFFF / #131B2C), selected row = bg-selected (#EEF0FF / #1F2A55). Reverted the earlier chat-base → bg-default change (the light chat canvas is #F6F8FA, not white) and the dark rail (it's #131B2C, not #0A101C). Every sampled surface maps to an existing token — no new values.--aira-bg-default (#FFFFFF / #0E1424), --aira-bg-subtle, --aira-bg-muted, --aira-bg-inverse, --aira-bg-elevated (#FFFFFF / #131B2C); text --aira-text-inverse, --aira-text-danger, --aira-text-success, --aira-text-warning; and border --aira-border-brand (brand-600 #3438C9 / indigo-400 #6268FF). Exact duplicates are aliased to existing tokens (no drift); all are additive.#F6F8FA / #0E1424); cards / KPI tiles on #FFFFFF / #131B2C; the left rail on #FFFFFF / #0A101C. On-surface text is documented with measured contrast — primary #111827 / #F4F6FA (AAA), secondary #364152 / #C8CFDB (AAA), tertiary #6B7785 / #94A0B4 (AA). Added named tokens --aira-surface-chat-bg, --aira-surface-artifact-bg, --aira-surface-artifact-card-bg, --aira-surface-rail-bg and a Surfaces & on-surface text spec (surface map + contrast tables + a live light/dark preview) in Color.allowFreeText, freeTextPlaceholder, maxRows, submitOnEnter, onSubmitText) and a textarea row in the interaction matrix.viewBox 0 0 22 22, the supplied path + gradient — #6268FF → #3BA7FF, offset 0.255, horizontal userSpaceOnUse) across all 26 instances: top bar, Logo & marks, email headers, in-context mock, and the icon set. The old mark used a different viewBox and a diagonal gradient, so marks copied out of the system didn't match the brand asset. (The 5 px-radius selector was updated to the new viewBox too.).avatar.aira style, and stopped describing the sparkle as a per-message avatar or speaking/streaming indicator (Avatar, Logo & marks, cover, My Configs). Avatars are user-only.PanelLeft) from lucide-react to the icon library.:hover/:active, so it wins over the higher-specificity variant :hover rules — a hovered disabled button stays visually identical to its resting state (only the not-allowed cursor shows).lucide-react under aiRADS names via src/lib/icons.js (e.g. Send, Regenerate, Workbook — not the underlying Lucide names), with defaults size 20 · strokeWidth 1.5 · currentColor. The custom aiRA sparkle is kept as AiraSparkle (not in Lucide).--aira-* CSS custom-property prefix on every token (e.g. --aira-bg-surface, --aira-sp-3) for Ant Design v3 coexistence — matching the dev's tokens.css. ~1,900 var() references updated across CSS and HTML; generate.js now emits the prefix automatically.text-placeholder (2.5 : 1) as an intentional sub-AA hint that relies on the field label.AiraButton, AiraConversation, AiraDataTable…) plus an Engineering handoff block — the libraries it's built with and a React/Tailwind snippet.@aira/react library and its stack (@base-ui/react, cva/clsx/tailwind-merge, xstate, @tanstack/react-table + react-virtual, recharts/d3, lucide-react, date-fns, tailwindcss-animate). Added File shape, Prop conventions, Imports & tokens, Time & virtualization, and an a11y checklist.Aira/aira- code prefixes) to align with the developed design system. The assistant brand wordmark aiRA and the Logo & marks rules are intentionally unchanged.aria-sort still carry the active direction.aria-label), and all four action icons are sized consistently at 18 px.tokens/tokens.json, css/, html/components/ — with scripts/generate.js (tokens → CSS) and scripts/bundle.js (→ a self-contained dist/index.html).Write the summary so a designer or engineer who skipped this edition can act on it without opening anything else.
Backdate or rewrite an entry once an edition has shipped. Add a new Fixed entry instead.
List removals explicitly — including the reason and where the work moved to (e.g. Deferred).
Bury removals inside a "Changed" entry. Removed tokens and removed components get their own line.
The people responsible for the Aira Design System — the points of contact for design, development, and product decisions.
| Area | Responsible person |
|---|---|
| Design | Punya Shetty |
| Development | Keshav Agrawal · Vaibhav V Bhat |
| Product | Harsh Deo |