Aira Design System v1.1

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.

AI fluency fixes

Recurring copy fixes that keep aiRA's language fluent, honest, and on-voice. Pairs with Voice rules and Refusal grammar.

Instead ofWriteWhy
"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 & HeadingsSentence case everywhereSystem-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.

Principles

  • Can't / can / next. Every refusal names what it can't do, what it can, and the next step.
  • One aiRA. No "the AI", no per-agent personas, no coder agent.
  • Plain over precise-sounding. No jargon or error codes in user-facing copy.

Tokens & MCP

How design tokens reach code, and how aiRA reads the system to stay on-brand.

Token pipeline

  • tokens/tokens.json (DTCG 2025.10) is the source. scripts/generate.js emits css/variables.css — CSS custom properties for light and dark.
  • Prefix. Every custom property is emitted with the --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.
  • Tailwind reads the same tokens as its theme, so utilities like bg-surface, text-secondary, rounded-md resolve to token values — components never hard-code hex, spacing, or radius.
  • The same tokens.json publishes to Figma variables via Tokens Studio (see Figma bundle), so design, code, and Figma stay one source of truth.

Model Context Protocol (MCP)

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.

  • Tokens resource — the resolved light/dark token set.
  • Component map — AiraX names → props, variants, and the library each is built with.
  • Code Connect — Figma node → React source, so Dev Mode and aiRA point at the same implementation.

About this file

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.

Stack & sync

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:

  • Primitives & a11y — @base-ui/react
  • Variants & className — class-variance-authority · clsx · tailwind-merge
  • Styling & motion — Tailwind CSS (tokens as theme + CSS variables) · tailwindcss-animate
  • Icons — lucide-react
  • State — xstate · @xstate/react (conversation, approval, streaming)
  • Tables & lists — @tanstack/react-table · @tanstack/react-virtual
  • Charts — recharts · d3
  • Dates — date-fns

Each 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.

Non-goals

  • Not a runtime — this document ships no React; it's the spec the @aira/react library implements.
  • No agent-identity fiction (one aiRA; no per-agent themes) and no coder agent.
  • No design-only multi-brand constructs ahead of the developed v1 contract.

How to contribute

  • Change a value in tokens/tokens.json, then run node scripts/generate.js && node scripts/bundle.js.
  • Add or edit a component in html/components/<id>.html; keep its AiraX name and Engineering-handoff block in step with the Storybook.
  • When the design and the developed reference diverge, the developed reference wins for naming and terminology.

README

Aira Design System · v1.1

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 it

open aira-design-system/index.html

No build step. Toggle Light/Dark and LTR/RTL from the top nav.

Files

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

Atomic IA

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

Cross-cutting rules

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.

Source of truth

The component library ships as @aira/react (see the developer Storybook). This file is the design source of truth; for teams consuming it directly:

  • Markup — index.html. Copy demo HTML verbatim into your product; replace inline values with your data binding.
  • Tokens — tokens.css. Import once at the app root. Never redefine a token locally.
  • Behaviour — the data-ds-* hooks at the bottom of index.html (Tabs, Dropdown, sidebar search, Type-to-confirm).

Color

Two-tier system: raw primitives at the base, semantic tokens on top. Product code references semantic tokens only.

Slate · neutrals

slate-25#FBFCFD
slate-50#F6F8FA
slate-100#EEF1F4
slate-200#E2E6EB
slate-300#C9D0D9
slate-400#9AA4B2
slate-500#6B7785
slate-600#4D5867
slate-700#364152
slate-800#1F2937
slate-900#111827
slate-950#0A0F1A

Indigo · primary brand

indigo-600 (#3438C9) is the primary action colour — the fill used on every primary button across the system. Flagged below.

indigo-50#EEF0FF
indigo-100#DDE0FF
indigo-200#BAC1FF
indigo-300#8E97FF
indigo-400#6268FF
indigo-500#4A50EF
indigo-600#3438C9Primary button
indigo-700#252AA0
indigo-800#1B2078
indigo-900#121757

Sky · gradient end-stop

sky-50#E6F1FF
sky-100#C7E1FF
sky-200#8EC1FF
sky-300#5EA8FF
sky-400#3BA7FF
sky-500#1564CC

Status accents

green-500#1F7A42
amber-400#B97300
red-400#D92D2D
info-400#1F6AAA
violet-500#6E40D9

aiRA gradient

indigo-400 → sky-400. The single gradient used for the aiRA mark, send button, avatars, and any aiRA-anchored surface.

Semantic chips

Default Brand Success Warning Danger Info

Surfaces & on-surface text

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.

Light · artifact panel #FBFCFD · card #FFFFFF
ACTIVE MEMBERS
82,010
▲ 6% vs March
source · CDP snapshot, Apr 29
Left rail #FFFFFF
Dark · artifact panel #182135 · card #131B2C
ACTIVE MEMBERS
82,010
▲ 6% vs March
source · CDP snapshot, Apr 29
Left rail #131B2C

Surface map

Region (in-app)TokenLightDark
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

On-surface text · contrast

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 tokenLight hexOn lightDark hexOn dark
--aira-text-primary#11182717.8:1 · AAA#F4F6FA15.9:1 · AAA
--aira-text-secondary#36415210.5:1 · AAA#C8CFDB11.0:1 · AAA
--aira-text-tertiary#6B77854.6:1 · AA#94A0B46.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).

Developer semantic tokens

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.

Background

TokenLightDark
--aira-bg-default#FFFFFF#0E1424
--aira-bg-subtleslate-50 #F6F8FAslate-900 #111827
--aira-bg-mutedslate-100 #EEF1F4slate-800 #1F2937
--aira-bg-sunkenslate-100 #EEF1F4#0A101C
--aira-bg-canvas#F6F8FA#0E1424
--aira-bg-elevated#FFFFFF#131B2C
--aira-bg-inverseslate-900 #111827#FFFFFF
--aira-bg-brandindigo-600 #3438C9indigo-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).

Text

TokenLightDark
--aira-text-primaryslate-900 #111827#F4F6FA
--aira-text-secondaryslate-700 #364152#C8CFDB
--aira-text-tertiaryslate-500 #6B7785#94A0B4
--aira-text-linkindigo-600 #3438C9indigo-300 #8E97FF
--aira-text-inverse#FFFFFFslate-900 #111827
--aira-text-disabledslate-400 #9AA4B2#5A6678
--aira-text-dangerred-500 #B11E1E#F58B8B
--aira-text-successgreen-600 #185C32#6FD995
--aira-text-warningamber-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.

Border

TokenLightDark
--aira-border-subtleslate-100 #EEF1F4#1F2940
--aira-border-defaultslate-200 #E2E6EB#2A3654
--aira-border-strongslate-300 #C9D0D9#3A4870
--aira-border-brandindigo-600 #3438C9indigo-400 #6268FF

border-brand is for brand-emphasis edges — it is not the focus ring (--aira-border-focus, indigo-300 in dark).

Do / don't

Do

Reference semantic tokens (--aira-text-primary, --aira-surface-chat-bg) in product code.

Don't

Reference primitives (--aira-slate-700) or paste raw hex. They change between modes.

Do

Keep muted (tertiary) text on cards, where it clears AA in both modes.

Don't

Put small tertiary text straight on the canvas — it falls below 4.5:1 in light.

Color fidelity

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².

Why colors vary across machines

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).

What the system does

  • Brand gradients use OKLCH interpolation so the midpoint stays vibrant across display gamuts.
  • The dark canvas sits at #0E1424 — lifted slightly from true black so panels with backlight bleed render it as clearly dark rather than muddy grey-blue.
  • Brand surfaces opt out of Windows forced-color overrides so the indigo primary stays indigo in High Contrast Mode.
  • Light-mode rendering can't be hardware-corrected via CSS. Uncalibrated Windows panels with cool white points will render light surfaces with a faint blue cast that no code change resolves.

What product teams can do

  • Review on a calibrated display when possible. A software calibration via OS settings gets most of the way.
  • Spot-check on the cheapest Windows laptop common in your deployment — that's the floor.
  • Trust the tokens. Don't tweak hex values to compensate for what you see on your own machine.

What can't be fixed in code

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.

Typography

Roboto, Regular (400) + Medium (500) + Semibold (600). One ramp, used consistently.

Display lg · 40/48
Display md · 32/40
Heading 1 · 28/36
Heading 2 · 22/30
Heading 3 · 18/26
Heading 4 · 16/24
Body lg · 16/26 — long-form prose, marketing.
Body md · 14/22 — the default body size used everywhere.
Body sm · 13/20 — helper, captions, metadata.
Label lg · 15/22
Label md · 14/20
Label sm · 12/16
Overline · 11/16

Spacing

4 px base grid. Use the scale; no 10 px, 13 px, 22 px.

sp-14px
sp-28px
sp-312px
sp-416px
sp-520px
sp-624px
sp-832px
sp-1248px

Radius & elevation

Radii

Elevation

Motion

Three durations. prefers-reduced-motion halts animation — does not speed it up.

TokenValueUse
--motion-fast120ms · ease-outHover, focus, micro-toggles.
--motion-base200ms · ease-outDefault for most state changes.
--motion-slow320ms · ease-outLarge surfaces (sheets, modals).
--motion-reduced0.001msDetectable by JS when reduced-motion is on.

Iconography

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.

PlusUpload
MicVoice input
SendSubmit / send
StopHalt generation · circle + square
CopyCopy to clipboard
RegenerateRe-run prompt
ThumbsUpLike
ThumbsDownDislike
ChevronUpDisclose / expand caret
ChevronDownDropdown caret · collapse
ChevronLeftBack / previous
ChevronRightForward / next
PencilEdit / rename
CloseDismiss / remove
CheckConfirm / done
SearchFind
PanelLeftSide panel
SortSortable column · two opposing arrows
SortAscSort ascending
SortDescSort descending
MoreHorizontalRow menu
MoreVerticalToolbar menu
InfoInfo badge
WarningCaution
ErrorFailure
SuccessConfirmed · circle + check
LoaderAsync work
AiraSparkleaiRA mark · custom (not in Lucide)
DownloadDownload
ExternalLinkOpen elsewhere
LinkLink
EyeShow / view
EyeOffHide
LockLocked / RBAC
UnlockUnlocked
TrashDelete
ArchiveArchive
PausePause
PlayPlay / resume
LifeBuoySupport · action row below aiRA's responses
UploadUpload file
ServerFTP push · export marker
WorkbookWorkbook download · export marker
FileCodeCSS download · export marker
PaperclipAttach file
LightbulbInfo tip · how-to note

Interactive states

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.

StatePlusSendStopTrash
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.

Do / don't

Do

Import icons from lucide-react by name and let colour inherit via currentColor, so the theme tints them.

Don't

Embed PNGs or bake hex colours into icons. Breaks dark mode.

Do

Use the Stop icon (circle with a square inside) only when interrupting an in-flight aiRA generation.

Don't

Disable an agent. Agents stop generating; they're never disabled.

Engineering handoff

src/lib/icons.js Built with lucide-react
// 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

Data viz

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.

Five guidelines

  1. Show the data. Strip ornament. If the reader can't see the pattern you're communicating, simplify until they can.
  2. Reduce the clutter. No 3D, no gradients on chart marks, no heavy gridlines. Every non-data element earns its place.
  3. Integrate graphics and text. The title is the takeaway, not the topic. Annotate moments directly on the chart.
  4. Avoid the spaghetti chart. Past four-to-six series, highlight-and-gray or break into small multiples.
  5. Start with gray. Add colour only where it carries meaning. Forces intentional choices and creates visual hierarchy.

Perceptual ranking

Encodings ranked by how accurately the eye reads them (Cleveland & McGill). Use higher-ranked encodings when precision matters.

RankEncodingUsed in
1Position on a common scaleBar · Dot plot · Line
2Position on non-aligned scalesSmall multiples
3Length without shared baselineStacked bar segments
4Direction · slopeSlope chart
5AnglePie chart (use sparingly)
6AreaTreemap · Bubble
7Volume3D — avoid
8Colour saturation · hueHeatmap · Choropleth

Choosing the chart — by data relationship

You're showingGo-to chart types
Comparison across categoriesBar · Dot plot · Heatmap · Waffle
Change over timeLine · Area · Slope · Sparkline
DistributionHistogram · Box plot
Relationship between variablesScatterplot
Part-to-wholeStacked bar · Treemap · Waffle
Precise value lookupData table (see below)

Bar chart · vertical

The workhorse. Length on a common baseline reads accurately. Always start the value axis at zero. Sort by value unless a natural order exists.

HSR Layout leads on monthly sales, by 28%
Loyalty engine · sales in lakhs · May 2026
15 8 0 HSR Ind Kor Whi Jay MGR

Bar chart · horizontal

Use horizontal when labels are long. Sort descending by default.

Top redeeming channels this quarter
Share of total redemptions
WhatsApp Push Email In-app SMS Web 34% 28% 22% 16% 11% 7%

Paired bar chart

Two values per category, side by side. Limit to two groups; beyond that, switch to a slope chart or dot plot.

This quarter vs. last · top stores
Q2 2026    Q1 2026
HSR Ind Kor Whi

Stacked bar chart

Total + composition. Only the bottom segment shares a baseline — put the most important there. Limit to three to five segments.

Redemptions by channel · last 30 days
WhatsApp 34% Push 28% Email 22% In-app SMS

Diverging bar chart

Bars extend in two directions from a central baseline. Use for data with a natural midpoint — satisfaction, sentiment, percent change.

Δ vs. prev 30 days · top stores
+8.2% HSR +2.1% Ind −4.6% Kor −11.8% Whi +4.6% Jay

Dot plot · dumbbell

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.

Open rate · email vs. push, by tier
0% 50% 100% Platinum Gold Silver Base Email Push

Heatmap

Grid of cells coloured by value. Two categorical axes, one quantitative measure. Sequential palette for a single measure; diverging when there's a midpoint.

Redemption rate by day & hour
9a 12p 3p 6p 9p 12a 3a 6a Mon Tue Wed Thu Fri Sat Sun

Waffle chart

10×10 grid of squares. Each square is one percent. More tangible than a pie for a single statistic — "23 of every 100 members".

38% redemption rate · 38 of every 100 active members

Line chart

The default for time series. Limit to four-to-six lines; beyond that, highlight-and-gray or small multiples.

Active members grew 14% over six months
14k 12k 10k Dec Jan Feb Mar Apr May

Line chart · highlight-and-gray

When showing many series, color only the lines that carry the story. Everything else stays neutral.

HSR Layout pulled away from the pack in April
HSR Dec May

Area chart

Line chart with filled area below. Emphasises magnitude. Don't stack more than four-to-five series.

Redemptions, last six months
Dec May

Slope chart

Two vertical axes connected by lines. Shows direction and magnitude of change across many categories at once.

Tier movement, Q1 → Q2
Q1 Q2 Platinum 18% 22% Gold 34% 38% Silver 28% 25% Base 20% 15%

Sparkline

Tiny inline chart — no axes, no labels. Used in tables and KPI tiles to add trend context to a single number.

Active members
12,481
Redemption rate
38%
Tier upgrades
219

Histogram

Bars showing frequency per bin. Bars touch — gaps are reserved for categorical bar charts.

Days since last purchase · active members
0 30 60 90+ days since last purchase

Box-and-whisker plot

Median, IQR, and outliers in compact form. Requires statistical literacy — annotate parts for general audiences.

Basket size distribution by tier (₹)
0 5000 10000 Platinum Gold Silver

Scatterplot

Dots on two continuous axes — the go-to for showing correlation. Use transparency when dots overlap; label outliers directly.

Recency vs. basket size
Each dot is a member · sample of 80
0 5k 10k days since last purchase → linear fit

Treemap

Nested rectangles, area encodes value. For hierarchical data. Area is imprecise — use for broad patterns, not lookup.

Spend by category
Apparel 32% Beauty 18% Home 12% Tech 8% Grocery 14% Pharma 8% Other 8%

Pie chart · use sparingly

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.

Approved vs. rejected proposals
  • Approved · 72%
  • Rejected · 28%

Donut chart · use sparingly

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.

Redemption mix · last 30 days
42.1K redemptions
  • Points · 54%
  • Voucher · 28%
  • Tier-upgrade · 18%

Categorical · 6 series

Unordered groups. Distinct, equally vivid hues. Limit to six.

Channel · locked taxonomy

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.

WhatsApp
Push
Email
In-app
SMS
Web

Sequential · single hue

For ordered data (low → high). Single hue varying in lightness.

Diverging · with midpoint

For data with a natural neutral midpoint — sentiment, percent change. Two hues diverging from neutral.

Delta semantics

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.

+8.2%
vs. prev 30d
−4.6%
vs. prev 30d

Data table · with sparklines

Tables are visualisations too. Right-align numbers, light horizontal rules only where needed, sparklines for trend context.

Store performance · 6-month trend
Loyalty engine · May 2026
StoreSalesΔ vs prev6-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%

Table design — ten rules

  1. Remove or minimise borders and fills — let whitespace organise.
  2. Light, thin horizontal rules only where needed.
  3. Remove vertical rules entirely.
  4. Right-align numbers, left-align text.
  5. Consistent precision within a column.
  6. Guide the eye with alignment, not lines.
  7. Highlight key values with weight, colour, or background.
  8. Add sparklines or inline bars for trend context.
  9. Limit columns — past fifteen, split or chart.
  10. The title states the finding, not "Table 1".

Numeric formatting

  • Use tabular numerals so digits line up vertically in tables.
  • Match precision to what the reader needs — a compact "14.2L" / "1.4M" beats "1,421,540" in a dashboard.
  • Put the unit on the value, not on every gridline.
  • Round consistently. Don't show 8.234% next to 12% in the same column.
  • No hard-coded prefixes. The currency symbol, digit grouping (1,23,456 vs 123,456), and decimal style are resolved dynamically from the brand's locale and currency at render time — never baked into the component. The same chart renders ₹, $, or € purely from brand config.

Active titles

The title is the takeaway. "Active members grew 14% over six months" beats "Active members over time".

Accessibility

  • Never rely on colour alone — pair with shape, pattern, label, or direct annotation.
  • About 1 in 20 people have a colour-vision deficiency. Avoid red-green as the only distinction.
  • Charts must remain readable in grayscale.
  • Provide an accessible name on every chart SVG.

Implementation checklist

  • Title states the insight, not just the topic.
  • Non-data elements (gridlines, borders, fills) are minimal.
  • Colour is intentional, not decorative.
  • Chart type matches the data relationship.
  • Bar charts start at zero.
  • No 3D, no gradient fills on chart marks.
  • Series labelled directly where practical — not just a legend.
  • Chart works in grayscale.
  • Key findings called out by annotation.
  • Data source is credited in a subtitle or footnote.

Do / don't

Do

Start every bar chart's value axis at zero.

Don't

Truncate a bar axis to amplify the difference — it misleads.

Do

Use orange for negative deltas. Red is reserved for errors.

Don't

Use a pie chart with more than three slices.

Do

Highlight-and-gray when there are more than four to six series.

Don't

Use 3D charts or gradient fills on chart marks.

Engineering handoff

AiraBarChart · AiraLineChart · AiraHistogram · AiraMetricTile · … Built with recharts d3
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

Approval status tokens

Six lifecycle states with paired surfaces.

Draft Awaiting approval Upcoming Approved Live Rejected
  • Draft — not yet submitted.
  • Awaiting approval — submitted, with a reviewer.
  • Upcoming — approved and scheduled, not yet live.
  • Approved — signed off, no schedule required.
  • Live — currently active in production.
  • Rejected — declined; reason persisted on the card.

Code & syntax tokens

Single-tone palette; no carnival-coloured highlighting. Tokens used in Code block.

TokenUsed for
--syntax-keywordSELECT, function, const
--syntax-string'literal'
--syntax-number42, 3.14
--syntax-comment// remark
--syntax-operator=, +, AND
--syntax-functionCOUNT(), SUM()
--syntax-punctuation(), ;, ,

Safe area

Honour the device notch and home indicator on mobile surfaces (sheets, app bars).

Safe area content lives here
--safe-top --safe-bottom
Portrait — notch top, home indicator bottom
Safe area content lives here
--safe-left --safe-right
Landscape (iOS) — insets shift to the orientation edges
Safe area content lives here
status bar gesture nav
Android — status bar top, gesture nav bottom

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.

Tokens

TokenSource (web)iOS maps toAndroid maps to
--safe-topenv(safe-area-inset-top, 0px)Notch / Dynamic IslandStatus bar inset (incl. punch-hole)
--safe-bottomenv(safe-area-inset-bottom, 0px)Home indicatorGesture nav pill (or 3-button nav)
--safe-leftenv(safe-area-inset-left, 0px)Landscape notch sideDisplay cutout (landscape)
--safe-rightenv(safe-area-inset-right, 0px)Landscape home-indicator sideGesture 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.

Do / don't

Do

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.

Don't

Hard-code 44 px or 34 px insets. They differ per device, OS, and orientation — use the env-backed tokens.

Do

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".

Don't

Branch CSS by user-agent. The same --safe-* tokens cover both platforms — branching means a missed device.

Responsive

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.

Breakpoints

Mobile-first. Layouts collapse top-down.

TokenRangeColumns
sm0 – 599 px4
md600 – 1023 px8
lg1024 – 1439 px12
xl1440 px +12

Hit targets

Minimum tappable area per surface. Always pass the largest constraint that applies.

28
Dense table
36
Desktop default
44
Mobile / tablet
48
Material AAA

Type-scale tweaks

Headings step down one notch on sm; body stays.

RoleWebTabletMobile
Display40 / 4832 / 4028 / 36
H128 / 3624 / 3222 / 30
H222 / 3020 / 2818 / 26
Body14 / 2014 / 2015 / 22
Caption12 / 1612 / 1612 / 16

Platform consistency · Windows ↔ macOS ↔ iOS ↔ Android

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.

ConcernWeb (Win / Mac)iOS / iPadOSAndroid
Tap target36 px44 × 44 pt48 × 48 dp
Body text14 / 2015 / 22 (Dynamic Type)14 / 20 (sp)
Corner radius--radius-md 8 px10 px (continuous)12 px
Elevation--shadow-sm / md / lgsame shadow tokenssame + state-layer overlay
Focus ring2 px indigo + 3 px halosystem tint2 dp indigo
Iconography1.5 px stroke @ 20 px1.5 pt stroke @ 24 pt1.5 dp stroke @ 24 dp
Safe area—env(safe-area-inset-*)system bars · 56 dp top

Layout adaptation

How the aiRA chat surface restructures across breakpoints.

Mobile · sm
aiRA
Full-bleed chat. Side-nav becomes a drawer. Canvas stacks vertically.
Tablet · md
aiRA · Acme
Rail nav + single panel. Split panel collapses to tabs.
Desktop · lg / xl
Chat thread
Artifact preview

Do / don't

Do

Use the same semantic tokens at every breakpoint. Only the hit-target size and the type-scale notch change.

Don't

Re-skin a component per breakpoint. The tokens already handle theme; breakpoints only resize.

Do

Bump every tappable target to ≥ 44 px on mobile and ≥ 48 dp on Android.

Don't

Ship a 28 px target outside a dense table. 28 px is the floor for opt-in density only.

Voice rules

Voice is part of the design. Nine hard rules.

  1. Sentence case. Titles, buttons, labels — all sentence case. Avoid Title Case unless it's a proper noun.
  2. Active voice. "aiRA created the promotion" not "The promotion was created."
  3. One exclamation per first session. No exclamation marks elsewhere.
  4. No marketing language. "Set up the campaign" not "Unleash your campaign magic."
  5. Don't apologise reflexively. If aiRA can't do something, say what it can, then what's next.
  6. Don't anthropomorphise. aiRA does not "think", "feel", or "hope". It does work.
  7. Numbers in numerals, even for one-digit. "5 items left." Easier to scan.
  8. Plain verbs in activity rows. "Querying analytics" not "Executing query_engine_v3".
  9. Refusal grammar follows the can't / can / next template. See below.

Do / don't

Do

"Couldn't run that query — the table isn't connected yet. Connect it under Settings → Data."

Don't

"Sorry, sorry — I'll try again." Reflexive apologies clutter the surface.

Do

"aiRA created 1 promotion." Numerals scan faster than spelled-out one.

Don't

"aiRA thought you might like this!" Reads as artificial enthusiasm.

Refusal grammar — can't / can / next

When aiRA can't fulfil a request, the response has three parts: what it can't do, what it can, and what to do next.

I can't pull transaction data for the Acme brand — RBAC restriction.
I can show you aggregate spend by tier across the brands you have access to.
Ask your admin to add Analytics for Acme, or pick a different brand to query.

Button

StableUpdated Owner · Punya Shetty

Four variants, three sizes. Primary indigo-600 fill. The single canonical button atom — no per-context one-offs.

Variants

Sizes

Properties

PropValues
variantprimary · secondary · tertiary · danger
sizesm · md · lg
disabledboolean

Do / don't

Do

Use primary for the single confirming action; secondary for the alternative; tertiary for low-frequency.

Don't

Stack two primary buttons next to each other. There can only be one.

Do

Match button size to the surrounding density.

Don't

Use a danger button for routine destructive actions like Remove — use secondary.

Engineering handoff

AiraButton Built with cva clsx tailwind-merge
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

Icon button

StableUpdated Owner · Punya Shetty

Single-icon affordances. 32 px default; 44 px touch. Use the Stop icon only for halting an in-flight aiRA generation.

Sizes & states

Stop icon (Generating state)

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.

Used inside the Composer when aiRA is streaming. See Composer.

Do / don't

Do

Pair every icon button with an accessible name describing the action.

Don't

Pack more than three icon buttons in a row without visual separation.

Engineering handoff

AiraIconButton Built with cva lucide-react
import { AiraIconButton } from "@aira/react";
import { Upload } from "lucide-react";

<AiraIconButton aria-label="Upload" size="md"><Upload /></AiraIconButton>

Input

StableUpdated Owner · Punya Shetty

36 px default. Ten states.

Enter a valid email.
High multiplier — review.

Do / don't

Do

Show a label above the input. Placeholder is never a substitute for a label.

Don't

Trigger validation on every keystroke. Validate on blur or submit.

Do

Pre-fill the most likely value when one exists.

Don't

Use a generic Save error — name the field that failed.

Engineering handoff

AiraInput Built with @base-ui/react cva
<AiraInput value={v} onValueChange={setV} placeholder="aiRA" invalid={!!error} />

Textarea

StableUpdated Owner · Punya Shetty

Multi-line input. Resizable on the Y axis only.

Markdown is supported.

Do / don't

Do

Resize vertically only.

Don't

Auto-grow past six lines without showing a scroll affordance.

Engineering handoff

AiraTextarea Built with @base-ui/react cva
<AiraTextarea rows={3} value={v} onValueChange={setV} />

Chip

StableUpdated Owner · Punya Shetty

Neutral metadata tag, removable. Carries an optional dot for status semantics.

Default Brand Active Pending Blocked Scheduled

Do / don't

Do

Reserve colour semantics — green for success, amber for pending, red for danger.

Don't

Stack chips, tags, and badges in the same row — pick one primitive per context.

Engineering handoff

AiraChip Built with cva clsx
<AiraChip tone="success">Live</AiraChip>

Tag

StableUpdated Owner · Punya Shetty

Categorical label. Larger surface than a chip; no dot.

Loyalty Promotions Analytics

Do / don't

Do

Use Tag for categorical grouping (channel, type, owner).

Don't

Use Tag for status — that's a Pill.

Engineering handoff

AiraTag Built with cva
<AiraTag>Beta</AiraTag>

Badge

StableUpdated Owner · Punya Shetty

Count or numeric indicator. Sits on top of a parent surface.

3 12 99+

Do / don't

Do

Cap counts at 99+. Beyond that, the precise number is noise.

Don't

Use a badge for a state name — that's a Pill.

Engineering handoff

AiraBadge Built with cva
<AiraBadge count={3} />

Pill (status)

StableUpdated Owner · Punya Shetty

Lifecycle states across approvals, scheduled jobs, and content. Six variants.

Draft Awaiting approval Upcoming Approved Live Rejected

Properties

VariantToken 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

Do / don't

Do

Use the canonical state colours so users learn the lifecycle once.

Don't

Invent new state labels — extend the existing set instead.

Engineering handoff

AiraPill Built with cva
<AiraPill status="upcoming">Upcoming</AiraPill>

Avatar

StableUpdated Owner · Punya Shetty

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.

User avatar — initials

PS PS PS Initials = first + last name, capitalised.

Do / don't

Do

Use avatars for the human user only. aiRA has no avatar — its identity is the sparkle brand mark.

Don't

Render the sparkle mark as a speaking avatar or profile image beside aiRA's messages. It's a brand mark, not a character.

Do

Provide aria-label when there's no visible name.

Don't

Use random colours for user initials. The system has one avatar gradient.

Engineering handoff

AiraAvatar Built with cva clsx
<AiraAvatar name="Punya Shetty" size="sm" />
{/* user avatars only — aiRA has no avatar; use <AiraMark /> for the brand mark */}

Citation chip

StableUpdated Owner · Punya Shetty

RAG provenance. Real <a href> links. Hover lifts the border + text to brand.

Inline

Tier upgrades happen automatically when 90-day spend crosses the threshold 1Tier upgrade SOP. The system re-evaluates eligibility nightly 2Loyalty engine spec.

Hover & focus

Hover or keyboard-focus brightens the border (--citation-border-hover) and shifts the text to brand colour (--citation-text-hover).

Appended list

Customer 88959517 wasn't issued points because the transaction amount was below the promotion's minimum threshold.

Do / don't

Do

Number citations sequentially in reading order.

Don't

Cite more than four sources inline. Use the appended list.

Engineering handoff

AiraCitationChip Built with cva
<AiraCitationChip index={1} href="#source-1">Proposal #4421</AiraCitationChip>

Tooltip

StableUpdated Owner · Punya Shetty

Short explainer on hover/focus. Surface is --tooltip-bg (slate-700 — softer than near-black) with white text at about 10 : 1 contrast.

Do / don't

Do

Use Tooltip for short, optional context. Trigger on hover AND focus.

Don't

Hide essential information inside a tooltip. Surface it inline.

Engineering handoff

AiraTooltip Built with @base-ui/react tailwindcss-animate
<AiraTooltip content="Copy">
  <AiraIconButton aria-label="Copy"><Copy /></AiraIconButton>
</AiraTooltip>
// wraps @base-ui/react Tooltip; enter/exit via tailwindcss-animate

Keyboard (kbd)

StableUpdated Owner · Punya Shetty

Visualises a key in a shortcut. Use sparingly — inline help, command palette docs, the Selection card keyboard hint.

↑↓EnterEsc⌘K

Shortcuts — Mac & Windows

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.

ActionMacWindows
Command palette⌘ KCtrl K
Slash commands//
Send message⌘ EnterCtrl Enter
New line in composer⇧ EnterShift Enter
Open help / shortcuts⌘ /Ctrl /
Stop generationEscEsc

Engineering handoff

AiraKbd Built with cva
<AiraKbd>⌘</AiraKbd> <AiraKbd>K</AiraKbd>

Divider

StableUpdated Owner · Punya Shetty

1 px horizontal rule using --border-default.

Section A
Section B

Engineering handoff

AiraDivider Built with @base-ui/react
<AiraDivider />   {/* @base-ui/react Separator */}

Spinner

StableUpdated Owner · Punya Shetty

Indeterminate progress. 14 px default.

Loading…

Engineering handoff

AiraSpinner Built with tailwindcss-animate
<AiraSpinner size="sm" />   {/* animate-spin */}

Skeleton

StableUpdated Owner · Punya Shetty

Placeholder for loading content. Hide from assistive tech (aria-hidden).

Engineering handoff

AiraSkeleton Built with tailwindcss-animate
<AiraSkeleton className="h-4 w-40" />   {/* shimmer */}

Field

StableUpdated Owner · Punya Shetty

Composed label + control + helper + error. The shape every form control uses.

Visible internally only. 1–60 characters.

Accessibility

  • Pair label[for] ↔ input[id].
  • Use aria-describedby to associate the helper with the input.
  • Error message: announce via aria-describedby in addition to the visual border colour.

Engineering handoff

AiraField Built with @base-ui/react
<AiraField label="Email" hint="Work address" error={error}>
  <AiraInput name="email" />
</AiraField>
// @base-ui/react Field wires label, description & validity

Checkbox

StableUpdated Owner · Punya Shetty

Multi-select within a group. Wrap with <fieldset> + <legend>.

Test scenarios

Do / don't

Do

Wrap a group with a fieldset and a legend so the group has a name.

Don't

Use a single standalone checkbox for binary on/off — that's a Toggle.

Engineering handoff

AiraCheckbox Built with @base-ui/react
<AiraCheckbox checked={on} onCheckedChange={setOn}>Email me</AiraCheckbox>

Radio

StableUpdated Owner · Punya Shetty

Exclusive selection. Always wrap in <fieldset> + <legend>. The disabled state is documented explicitly.

Reward type

States

  • Default — empty circle.
  • Selected — filled brand dot inside the circle.
  • Disabled — greyed, cursor not-allowed.
  • Disabled-selected — selected and locked. Used when policy pins a value.

Do / don't

Do

Always pre-select the safest, most-common default.

Don't

Use radio for more than five options. Switch to a Dropdown.

Engineering handoff

AiraRadio Built with @base-ui/react
<AiraRadioGroup value={v} onValueChange={setV}>
  <AiraRadio value="gold">Gold tier</AiraRadio>
</AiraRadioGroup>

Toggle

StableUpdated Owner · Punya Shetty

Binary on/off that takes effect immediately. For settings that need an explicit Save, use a Checkbox in a form.

Do / don't

Do

Reflect immediate effect — no Save button.

Don't

Use a Toggle when the change needs a confirmation. Use a Modal.

Engineering handoff

AiraToggle Built with @base-ui/react
<AiraToggle checked={on} onCheckedChange={setOn} />   {/* @base-ui/react Switch */}

Stepper

StableUpdated Owner · Punya Shetty

Ordered multi-step flow.

✓Draft
✓Submit
3Review
4Approve
5Live

Engineering handoff

AiraStepper Built with cva
<AiraStepper value={n} min={0} max={10} onValueChange={setN} />

Progress bar

StableUpdated Owner · Punya Shetty

Determinate progress for known durations.

Engineering handoff

AiraProgress Built with @base-ui/react
<AiraProgress value={62} />   {/* @base-ui/react Progress */}

Feedback control

StableUpdated Owner · Punya Shetty

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.

Default
Thumbs-up (like) selected
Thumbs-down (dislike) selected

Do / don't

Do

Render the thumbs as bare icon buttons, sitting inline with the action row. No pill, no border, no divider.

Don't

Wrap the controls in a chip, pill, or bordered container. The icons themselves are the affordance.

Do

Record the like / dislike signal inline and immediately. The thumb fill is the only confirmation the user needs.

Don't

Open a reason picker or modal prompt on dislike. The thumb signal is enough — don't interrupt the user to ask why.

Engineering handoff

AiraFeedback Built with lucide-react
import { ThumbsUp, ThumbsDown } from "lucide-react";

<AiraFeedback value={vote} onValueChange={setVote} />
// thumbs = like / dislike; dislike records inline, no reason picker

Action row

StableUpdated Owner · Punya Shetty

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.

Engineering handoff

AiraActionRow Built with lucide-react
<AiraActionRow onCopy={copy} onSupport={openHelp} value={vote} onVoteChange={setVote} />
// Copy · Support · Feedback — icon-only (lucide-react)

Tabs

StableUpdated Owner · Punya Shetty

Local navigation between views of the same data.

Top 10 stores by last-month sales.

Engineering handoff

AiraTabs Built with @base-ui/react
<AiraTabs value={tab} onValueChange={setTab}
  items={[{ id: "result", label: "Result" }, { id: "sql", label: "SQL" }]} />
// @base-ui/react Tabs (roving focus, aria-controls)

Popover

StableUpdated Owner · Punya Shetty

Rich content with actions. Footer buttons are right-aligned; primary on the far right.

Rule

Popover footer actions are right-aligned. Primary action sits on the far right. Tertiary or destructive actions sit to its left.

Do / don't

Do

Right-align footer actions; primary on the far right.

Don't

Hide essential information in a popover. Surface it inline.

Engineering handoff

AiraPopover Built with @base-ui/react tailwindcss-animate
<AiraPopover trigger={<AiraButton>Filters</AiraButton>}>…</AiraPopover>
// @base-ui/react Popover; positioned with its anchor

Toast

StableUpdated Owner · Punya Shetty

Transient notification. Auto-dismisses; reserves for non-critical confirmations.

Copied to clipboard
Promotion config copied as YAML.
Couldn't save draft
Network error. Retrying…

Do / don't

Do

Auto-dismiss after about four seconds.

Don't

Stack more than three toasts. Collapse the rest into a notification centre.

Engineering handoff

AiraToast Built with @base-ui/react tailwindcss-animate
const { toast } = useAiraToast();
toast({ title: "Promotion created", tone: "success" });
// @base-ui/react Toast provider

Nudge card

StableUpdated Owner · Punya Shetty

Suggestion or prompt presented next to the chat surface. Blue left-border accent.

Try Analytics for weekend trends
aiRA spotted that 38% of your top customers spend on Saturdays. See a breakdown by tier.
Heads up — fraud lookup may take longer today
A queued backfill is running. Results are accurate; expect 30–60 s response times.

Do / don't

Do

Render adjacent to the chat surface, never on top of it.

Don't

Stack two nudges. One per session.

Engineering handoff

AiraNudgeCard Built with cva lucide-react
<AiraNudgeCard title="38% of top customers spend on Saturdays" onDismiss={hide} />

Selection card

StableUpdated Owner · Punya Shetty

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.

Default — pick one

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.

What kind of promotion are you creating?

1 of 2
  1. 1Points multiplier on purchases
  2. 2Bonus points (flat reward)
  3. 3Coupon or discount code
  4. 4Tiered spend reward
↑↓ to navigate · Enter to select · 1–5 jumps · Esc skips

Free text — type a paragraph

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.

Tell aiRA what you're trying to do

⏎ to send · Shift+⏎ for a new line

Free text — filled (paragraph)

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.

Describe the win-back journey

⏎ to send · Shift+⏎ for a new line

Open-ended — no preset options

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.

What should this segment include?

Optional
aiRA turns this into a rule you can review

Variants

VariantWhen to useComposer
Pick one2–5 likely interpretationsCollapsed — opens from "Something else"
Free textUser picked "Something else"Expanded, empty, auto-grows
Free text · filledReviewing / editing a typed answerHolds a paragraph; scrolls past ~7 lines
Open-endedOptions can't be enumeratedComposer only, no list

Interaction matrix

Option rows and "Something else" use different hover surfaces so they read as separate affordances; the free-text field is a real, focusable input.

ElementDefaultHoverFocus (keyboard)Selected / active
Option rowtransparent--aira-bg-hoverfocus-visible ring--aira-bg-selected + filled number badge
"Something else"transparent, 1 px top divider--aira-bg-accent-subtle (distinct violet tint)focus-visible ringaria-expanded="true" — reveals the composer
Free-text field2 lines, placeholdern/afocus ring on the field (focus-within)grows with content to ~7 lines, then scrolls
Senddisabled (empty)--aira-btn-primary-bg-hoverfocus-visible ringenabled on non-empty text
Disclosure chevronpoints down (collapsed)row hover appliesrow focus-visible ringrotates 180° when the composer is open
Dismiss (×) / Escheader icon, top-right--aira-bg-hoverfocus-visible ringcloses the card (skips the question)

Properties

PropValues
titleThe question being asked
optionsArray of 2–5 strings. Omit for the open-ended variant.
step{ current, total } when chained
allowFreeTextboolean (default true) — "Something else" opens the composer
freeTextPlaceholderstring — placeholder shown in the empty composer
maxRowsnumber (default 7) — rows the box grows to before it scrolls
submitOnEnterboolean (default true) — Enter sends, Shift+Enter adds a line
onSelectFires on click, Enter, or numeric key (1–5)
onSubmitTextFires on Send or Enter with the typed paragraph
onSkipFires on Esc or the × (dismiss) — skips the question

Do / don't

Do

Use when the prompt has 2–5 likely interpretations. Numbered options are faster than prose.

Don't

Use for more than 5 options. Switch to a Dropdown (searchable).

Do

Always offer free text and a way out (× / Esc). "Something else" must open a real, focusable input — never a dead end.

Don't

Cap the box at one line or truncate. Let it grow with the answer and scroll past ~7 lines.

Do

Keep the composer in the card. Typing here is the answer to this question.

Don't

Block the main composer while a Selection card is up. Typing there dismisses the card.

Engineering handoff

AiraSelectionCard Built with @base-ui/react
<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}
/>

To-do list

BetaUpdated Owner · Punya Shetty

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.

Default

2 of 5 done

Anatomy

  • Item — checkbox + label. Whole row is clickable. Hover lifts background to hover surface.
  • Done state — checkbox fills with brand colour; label strikes through and shifts to tertiary text.
  • Progress line — "X of N done", live-region-polite so screen readers announce updates.

Interactions

  • Mouse — click anywhere on the row to toggle.
  • Keyboard — Tab moves between items; Space toggles. Each item is a real input[type="checkbox"].
  • Transition — 200 ms fade + strike-through (reduced-motion replaces with an instant snap).

Do / don't

Do

Use for short, ordered lists (3–8 items) where each row is a discrete task.

Don't

Use for long, unstructured task lists. Past ~8 items, switch to a Data table with status.

Do

Animate the strike-through on toggle (200 ms). Builds visual confirmation.

Don't

Hide completed items by default. Crossed-out items show progress and remind the user what was done.

Engineering handoff

AiraTodoList Built with @base-ui/react
<AiraTodoList items={items} onToggle={toggle} />
// progress label derived from checked count

Chat message

StableUpdated Owner · Punya Shetty

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.

Variants

Show me last month's sales by store.
Here's last month's sales by store. Top performer is HSR Layout at ₹14.2L.
aiRA reused the cached query plan from 2 minutes ago.

Anatomy

  • Thread — role="log" aria-live="polite"; new messages announce.
  • User message — grey pill, end-aligned, max 80% width.
  • aiRA reply — free-flowing body with no avatar and no badge above the prose. Body wraps in role="status" aria-live="polite".
  • System — single quiet line in tertiary text. Metadata, not announced.
  • Error — inline alert block with role="alert".

Do / don't

Do

Let aiRA's reply free-flow as plain text. No avatar or sparkle mark — the prose stands on its own.

Don't

Anchor aiRA's reply with an avatar, sparkle mark, bubble, or agent-name badge. There are no agent labels and no avatar.

Engineering handoff

AiraChatMessage Built with cva date-fns lucide-react
<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

Composer

StableUpdated Owner · Punya Shetty

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.

Default · empty (idle)

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.

Default · with text

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.

Generating state

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.

Read-only (conversation locked)

For historical or shared sessions where the user can read but not send.

Do / don't

Do

Swap the Send icon for the Stop icon while aiRA generates.

Don't

Disable the composer because aiRA is busy. Let the user keep typing.

Engineering handoff

AiraComposer Built with @xstate/react lucide-react
<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)

Approval card

BetaUpdated Owner · Punya Shetty

aiRA proposal awaiting human review. Header is the proposal title only — no agent badge. High-risk variants use the Type-to-confirm pattern.

Default

2× points on weekends · Gold tier

aiRA generated this promotion from your prompt, review the diff and edit or create.

3name: "Gold weekend boost"
4multiplier: 2
5applies_to: ["saturday","sunday"]
6audience: gold_tier
Awaiting approval

Status variants

Six lifecycle states — these status variants are mainly for configurations. Status pill on the left of the footer; primary action shifts per state.

Weekend boost · Gold tier

Draft — not yet submitted for approval.

Draft

Weekend boost · Gold tier

Submitted 4 min ago · with Harsh Deo for review.

Awaiting approval

Weekend boost · Gold tier

Approved by Harsh Deo · scheduled to go live Sat at 00:00.

Upcoming

Weekend boost · Gold tier

Approved by Harsh Deo · 2 hours ago.

Approved

Weekend boost · Gold tier

Live since Sat 00:00 · 1,247 redemptions so far.

Live

Weekend boost · Gold tier

Rejected by Harsh Deo · 2 hours ago.

Multiplier is 2× — too aggressive for the Gold tier this quarter. Try 1.5× and resubmit.
Rejected

High-risk · Type-to-confirm

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.

3× multiplier · All members

High risk · production

Applies to all 1.2 M members; irreversible once live. Confirm by typing APPROVE.

3audience: gold_tier
4multiplier: 2
4multiplier: 3
5audience: all_members # scope expanded
Awaiting approval

Diff overflow behaviour

Lines in diffTreatment
≤ 12Render in full. No scroll, no collapse.
13 – 40Max-height 320 px with internal scroll. Caption above the diff: "12 of 40 lines visible".
> 40Collapse 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 payloadReplace diff with a before/after preview. Never paste raw bytes.

Risk flagging rules

  • Production-affecting — any change whose audience or schedule expands. Surface a risk pill in the header.
  • Irreversible — deletions, archival, schema changes. Require Type-to-confirm.
  • Security-relevant — RBAC, audience PII filters, data-source connections. Approval shows the reviewer's name + timestamp; an audit-log entry is written.
  • Financial — multipliers, coupon values, payout caps. Require Type-to-confirm + restate the financial impact in plain language.

Do / don't

Do

Show the diff or preview inline up to twelve lines; collapse beyond that.

Don't

Reuse the routine Accept styling on high-risk proposals. Use Type-to-confirm.

Engineering handoff

AiraApprovalCard Built with @xstate/react cva date-fns
<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

Code block

StableUpdated Owner · Punya Shetty

First-class output for SQL and scripts. Language badge and a Copy action.

JavaScript
function normaliseRow(row) {
  // Drop fields not in target schema
  const { id, amount, currency } = row;
  return { id, amount: Number(amount), currency: currency || 'INR' };
}

Engineering handoff

AiraCodeBlock Built with lucide-react
<AiraCodeBlock lang="javascript" showCopy>{code}</AiraCodeBlock>

Inline diff

StableUpdated Owner · Punya Shetty

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.

What aiRA will change — Gold tier benefits
1tier: gold
2benefits:
3 points_multiplier: 1.5
3 points_multiplier: 2.0
4 free_shipping: true
5expiry: 90_days
− Removed — the old value + Added — the new value · Unchanged context

How to read it

  • A red line followed by a green line on the same line number is one edit. Here, line 3 changes points_multiplier from 1.5 to 2.0 — read it top-to-bottom as old → new.
  • A lone green + line is brand-new. Line 4 adds free_shipping; nothing was removed.
  • Sign and colour always travel together (−/red, +/green), so the diff still reads in greyscale and for colour-vision deficiency — never colour alone.
  • Context lines stay so you can see where the change lands in the surrounding config.

Do / don't

Do

Pair sign and colour — plus for added, minus for removed.

Don't

Hide context lines. Surrounding context tells the reviewer where the change lands.

Engineering handoff

AiraInlineDiff Built with d3
<AiraInlineDiff before={oldYaml} after={newYaml} />
// line diff (d3-array); +/− paired with colour for grayscale & CVD

Agent activity

StableUpdated Owner · Punya Shetty

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.

Single-step

One row: spinner + step verb + optional detail. Use plain verbs — no internal job IDs.

Querying analytics DB · last 30 days of sales

Multi-step · todo list

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.

Prompt enriched with brand context
Intent interpreted · build ingestion dataflow
Generating block sequence…
Validating outputs
Preview & explain

Do / don't

Do

Use plain verbs. "Querying analytics DB" — not "Executing query_engine_v3".

Don't

Surface internal job IDs.

Do

Show duration on completed steps.

Don't

Spin forever. Past 10 s, append "Still working — taking longer than usual" inline.

Engineering handoff

AiraAgentActivity Built with @xstate/react
<AiraAgentActivity steps={steps} current={i} />
// reflects thinking / streaming states from the conversation machine

Conversation state machine

BetaUpdated Owner · Punya Shetty

An AI turn has seven stops. The table below maps every state to its visible UI, user affordance, and live-region behaviour.

StateVisible UIUser affordanceLive 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.

Transition rules

Each transition is driven by exactly one trigger. Read every row as: from this state, this trigger moves the turn to the next state.

FromTriggerTo
idleUser types, then sendsuser-sent
user-sentaiRA begins workingthinking
thinkingFirst content token arrivesstreaming
thinkingCancel (or Esc)aborted
streamingStop clicked — partial output keptaborted
streamingLast token arrivesdone
thinking · streaming · abortedSomething failserror
errorRetry — re-sends the original promptuser-sent
done · aborted · errorUser composes a follow-upuser-sent

Two rules hold across every transition:

  • No empty streaming. thinking → streaming fires only on the first real content token. If thinking runs past 10 s with no token, append "Still working — taking longer than usual" to the activity row; never auto-cancel.
  • aiRA's state never disables the composer. While streaming, the Send icon swaps to the Stop icon (Stop, don't disable). Read-only treatment is reserved for locked conversations — shared sessions and audit views.

Engineering handoff

AiraConversation Built with xstate @xstate/react
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

Bottom sheet

StableUpdated Owner · Punya Shetty

Mobile escalation of Modal. Honours --safe-bottom.

Do / don't

Do

Pad the bottom with safe-bottom so the action row clears the home indicator.

Don't

Stack content beyond 95% height — route to a page instead.

Engineering handoff

AiraBottomSheet Built with @base-ui/react tailwindcss-animate
<AiraBottomSheet open={open} onOpenChange={setOpen}>…</AiraBottomSheet>
// @base-ui/react Dialog (bottom placement); honours safe-area insets

List row

StableUpdated Owner · Punya Shetty

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).

Sessions
2× points · Gold weekends
2m ago
3
Why didn't customer 88959517 get points?
30m ago
Last month sales by store
2h ago
2
Acme tier-2 redemptions vs. prev quarter
Yesterday
1
Draft Diwali campaign brief
3d ago
Chat surface (selected session renders here)
Export marker — what each pip means
Server — session output was pushed to an FTP destination
Spreadsheet — the workbook (.xlsx) was downloaded
Code file — the CSS file was downloaded
3 Total exports across all kinds (FTP + workbook + CSS)

Anatomy

  • New chat button. The + 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.)
  • Width. 260 px on desktop. The rail is narrow on purpose so the chat surface owns the viewport.
  • Row. One title (max two lines, ellipsis after), one timestamp, and an optional export marker on the trailing edge.
  • Selected state. Active session uses --bg-selected with a 3 px --border-focus leading border.
  • Export marker. Up to three distinct icon pips (FTP / workbook / CSS) followed by a total count. Repeats of the same type collapse into the single pip — the count carries the multiplicity.

Do / don't

Do

Wrap the whole row in a single interactive target. The export marker is a tooltip, not a separate link.

Don't

Split actions into multiple click targets on the same row.

Do

Show one pip per distinct export type. The count carries multiplicity.

Don't

Repeat the same pip three times for three exports of the same kind. The pip is the type; the count is the volume.

Engineering handoff

AiraListRow · AiraListRail Built with @tanstack/react-virtual
<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

Data table

StableUpdated Owner · Punya Shetty

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.

Default · sortable

Store Sales Txns Δ vs prev 30d Status
HSR Layout₹14,21,5403,219+8.2%Active
Indiranagar₹11,03,2102,884+2.1%Active
Koramangala₹9,84,7202,440−4.6%Active
Whitefield₹7,21,3101,940−11.8%Upcoming

Sort states

One icon, three states. Shape stays the same on every header; colour and aria-sort carry the meaning.

StateIcon shapeIcon colouraria-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

Do / don't

Do

Use the same sort icon on every column header. Consistency reads as one control, repeated.

Don't

Change the icon's shape when a column becomes active. Keep the two-arrow sort glyph and let colour + aria-sort carry the direction.

Do

Right-align numbers; use font-variant-numeric: tabular-nums.

Don't

Use red for negative deltas. Orange (delta-negative) is the chart palette default.

Do

Wire aria-sort on the header — assistive tech reads it.

Don't

Pair a separate filter icon with the sort. The data table sorts; it doesn't filter.

Engineering handoff

AiraDataTable Built with @tanstack/react-table @tanstack/react-virtual
import { AiraDataTable } from "@aira/react";

<AiraDataTable columns={columns} data={rows} />
// sorting/columns via @tanstack/react-table; rows virtualized with @tanstack/react-virtual

Card

StableUpdated Owner · Punya Shetty

Generic container. Use only when the content needs visual elevation; otherwise prefer a plain section.

Loyalty health · Last 30 days

1,247 active members · 38% redemption rate · 12% new tier upgrades.

Do / don't

Do

Use Card only when the content needs elevation; otherwise a plain section is enough.

Don't

Nest cards inside cards.

Engineering handoff

AiraCard Built with cva
<AiraCard title="Weekend boost" footer={<AiraButton>Open</AiraButton>}>…</AiraCard>

Form

StableUpdated Owner · Punya Shetty

Standard composition — field stack + actions footer.

Do / don't

Do

Keep the action footer right-aligned; primary on the far right.

Don't

Hide the submit affordance below the fold.

Engineering handoff

Form (composition) Built with @base-ui/react
<form onSubmit={onSubmit}>
  <AiraField label="Program name"><AiraInput name="name" /></AiraField>
  <AiraButton type="submit">Save</AiraButton>
</form>
// validation surfaced through @base-ui/react Field

Change-log feed

BetaUpdated Owner · Punya Shetty

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.

Today · 13 May 2026
Harsh Deo · Approved · 11:42
Approved Weekend boost · Gold tier — scheduled to go live Sat 00:00.
1Proposal #4421
aiRA · Changed · 11:30
Updated multiplier from 1.5× to 2.0×.
3 - multiplier: 1.5
3 - multiplier: 2.0
Yesterday · 12 May 2026
Punya S. · Added · 16:08
Created promotion draft Weekend boost · Gold tier.
Punya S. · Reverted · 14:22
Reverted earlier change to audience; scope narrowed back to Gold tier.
Older
Vaibhav V Bhat · Removed · 10 May
Removed deprecated auto_apply flag from program settings.

Anatomy

  • Filter strip — change-type multi-select + date range.
  • Day group — header label above an entries cluster.
  • Entry — actor avatar · name · change-type tag · timestamp · summary · optional diff or citation.
  • Timeline — vertical gutter with a small dot per entry.
  • Empty state — "No changes in this period" with a "Clear filters" CTA.

Change-type tags

Added Changed Removed Fixed Approved Reverted

Do / don't

Do

Group entries by day; lead with actor and change type.

Don't

Include entries with no summary. Every entry must say what changed.

Engineering handoff

AiraChangeLogFeed Built with date-fns @tanstack/react-virtual lucide-react
<AiraChangeLogFeed entries={entries} groupBy="day" />
// relative timestamps via date-fns; long feeds virtualized with @tanstack/react-virtual

Empty states

StableUpdated Owner · Punya Shetty

Specific copy that names what's missing and offers a next step. Never a generic "No results".

No matches for "xyz"

Try a shorter query, or search by promotion ID. You can also reset filters to see everything in this brand.

Loading & streaming

StableUpdated Owner · Punya Shetty

Three loading patterns. Choose by the work being done.

Spinner — single short task

Loading…

Skeleton — content shell

Agent activity

Drafting promotion config

Engineering handoff

AiraSpinner · AiraSkeleton · AiraAgentActivity Built with @xstate/react tailwindcss-animate
// streaming is a state machine, not a boolean
const [state] = useMachine(conversationMachine);
{state.matches("thinking") && <AiraAgentActivity … />}
{state.matches("streaming") && <StreamedTokens … />}

Error handling

StableUpdated Owner · Punya Shetty

Errors are first-class UI. Plain-language reason, then what to do next. Never a stack trace.

My Configs

StableUpdated Owner · Punya Shetty

A list of saved configurations the user can reuse — list-row organism with the config name and a status pill.

Weekend boost · Gold tier
Updated 2m ago
Upcoming
Diwali bonus · All members
Updated yesterday
Live
Birthday month · Auto
Updated 5 days ago
Draft

Mobile preview

StableUpdated Owner · Punya Shetty

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.

Today · 13 May
2× points for Gold this weekend.
Got it. I drafted a promotion — review and send for approval.

Engineering handoff

AiraBottomSheet · AiraComposer Built with @base-ui/react
<AiraBottomSheet open={open} onOpenChange={setOpen}>…</AiraBottomSheet>
<AiraComposer voiceInput />   {/* mic affordance is first-class on mobile */}

In context

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.

Customer insights · April 2026
Auto-refreshed at 09:14
24,810
Tier 1 active
+18%
38,210
Tier 2 active
+11%
12,430
Lapsed 30d
−22%
420
VIP top 1%
+8%

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.

Engineering handoff

AiraMetricTile · AiraCard · AiraSparkline Built with recharts d3
<div className="grid grid-cols-2 gap-4">
  <AiraMetricTile label="Tier 1 spend" value="₹14.2L" delta={+12} />
  <AiraSparkline data={trend} />
</div>

Email templates

StableUpdated Owner · Punya Shetty

Four transactional templates aiRA sends. Shared rules apply to all four — listed once below.

Shared rules

  • Hardcoded hex values. Email clients strip CSS variables. Copy resolved values from tokens.css at build time.
  • Table-based layout for any structural layout that must survive Outlook 2016+ on Windows. Flex and grid are not safe.
  • One CTA. Never two primary buttons of equal weight.
  • Width 560 px max. Wider templates clip on mobile clients.
  • No gradients in email. Solid colours only.
  • No tracking pixels. Use Capillary's transactional pipeline only.
  • Reply-to a routable address if the email surfaces reply-based affordances.

Template · Share session

Sent when a user shares a chat session with a colleague.

Template · Approval request

Sent when a reviewer is asked to approve an aiRA proposal.

Template · Scheduled-run result

Sent when a scheduled aiRA job completes (daily / weekly digest).

Template · Daily digest

A morning summary of pending items the user owns.

Do / don't

Do

Render diffs as text in the email. Survives mobile zoom, alt-text fallback, and assistive tech.

Don't

Send a "click to view in browser" stub. Put the decision in the email.

Accessibility

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.

Checked contrast — light mode

PairingRatioWCAG
text-primary on bg-surface17.7 : 1AAA
text-secondary on bg-surface10.3 : 1AAA
text-tertiary on bg-surface4.6 : 1AA
btn-primary-text on btn-primary-bg8.2 : 1AAA
tooltip-text on tooltip-bg (slate-700 surface, white text)10.3 : 1AAA
citation-text-hover on citation-bg-hover9.7 : 1AAA
status-upcoming-text on status-upcoming-bg5.0 : 1AA
text-placeholder on bg-surface2.5 : 1Hint only — see note

Checked contrast — dark mode

PairingRatioWCAG
text-primary on bg-surface15.9 : 1AAA
tooltip-text on tooltip-bg (slate-100 surface, slate-900 text)15.7 : 1AAA
btn-primary-text on btn-primary-bg (indigo-500, white text)5.7 : 1AA
status-upcoming-text on status-upcoming-bg8.0 : 1AAA

Hard rules

  • Hit area ≥ 44 × 44 px (touch) / ≥ 32 × 32 px (mouse-only desktop).
  • focus-visible on every interactive element.
  • Status communicated by colour AND text/icon — never colour alone.
  • prefers-reduced-motion halts animation (sparkle, todo strike-through, shimmer).
  • Form labels connected via for/id or aria-labelledby; errors via aria-describedby.
  • Avatars without a visible name carry aria-label.

Feedback routing

Five surfaces for telling the user something. Pick by urgency and persistence.

SurfaceUse when…Persistence
ToastNon-critical confirmation that the user can ignore.Auto-dismisses ~4 s.
BannerPage-level state that should persist.Stays until dismissed or condition changes.
Nudge cardSuggestion adjacent to the chat surface; user can act or ignore.Stays until dismissed.
Inline errorField-level validation.Stays until the field is valid.
ModalThe user must act before continuing.Blocks the page until resolved.

Conventions worth raising

Command palette · slash commands

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.

Developer alignment · multi-brand

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.

The React library

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.

ConcernLibrary
Accessible primitives — dialog, popover, tabs, select, tooltip, switch, field…@base-ui/react
Variants & classNameclass-variance-authority · clsx · tailwind-merge
Iconslucide-react
Conversation & approval statexstate · @xstate/react
Tables & long lists@tanstack/react-table · @tanstack/react-virtual
Chartsrecharts · d3
Dates & relative timedate-fns
Enter / exit animationtailwindcss-animate

File shape

  • One component per file, named 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.
  • Behaviour with more than two states (composer, approval card, conversation) lives in an xstate machine — not in ad-hoc component state.

Prop conventions

  • Variants — variant / size / tone resolved by class-variance-authority; the consumer's className is merged last with tailwind-merge, so overrides win without specificity fights.
  • Controlled value — value + onValueChange (the @base-ui/react convention), not onChange with raw events.
  • Composition — asChild (Base UI) renders as a different element instead of wrapping.
  • Icon-only controls require an aria-label. Icons are lucide-react glyphs, sized with Tailwind (size-*) and coloured by currentColor.

Imports & tokens

  • Import components from @aira/react and charts from @aira/react/charts.
  • Tokens are consumed as the Tailwind theme + CSS variables — never hard-code a hex, spacing, or radius. Every custom property is --aira-*-prefixed (antd v3 coexistence). Numeric/currency formatting is dynamic per brand locale (never a hard-coded prefix).

Time & virtualization

  • All timestamps and relative times go through date-fns — no hand-rolled date math.
  • Any list that can exceed ~50 rows — chat history (AiraListRail), the change-log feed, dropdown options, data tables — is virtualized with @tanstack/react-virtual.

Accessibility checklist

  • A keyboard path for every interaction; the global :focus-visible ring (no per-component override).
  • aria-* wiring comes from @base-ui/react primitives — don't re-implement roles by hand.
  • WCAG AA contrast in both themes; touch targets ≥ 44 px; never colour alone (pair with shape or label).

Component status

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.

ComponentFigmaHTMLA11yStatus
Button✓✓✓Stable
Icon button✓✓✓Stable
Input✓✓✓Stable
Textarea✓✓✓Stable
Chip✓✓✓Stable
Tag✓✓✓Stable
Badge✓✓✓Stable
Pill (status)✓✓✓Stable
Avatar✓✓✓Stable
Citation chip✓✓✓Stable
Tooltip✓✓✓Stable
Keyboard (kbd)✓✓✓Stable
Divider✓✓✓Stable
Spinner✓✓✓Stable
Skeleton✓✓✓Stable
Field✓✓✓Stable
Dropdown✓✓✓Stable
Checkbox✓✓✓Stable
Radio✓✓✓Stable
Toggle✓✓✓Stable
Stepper✓✓✓Stable
Progress bar✓✓✓Stable
Feedback control✓✓✓Stable
Action row✓✓✓Stable
Tabs✓✓✓Stable
Breadcrumb✓✓✓Stable
Popover✓✓✓Stable
Toast✓✓✓Stable
Banner✓✓✓Stable
Nudge card✓✓✓Stable
Selection card✓✓✓Stable
To-do list✓✓PartialBeta
Chat message✓✓✓Stable
Composer✓✓✓Stable
Approval card✓✓✓Beta
Code block✓✓✓Stable
Inline diff✓✓✓Stable
Agent activity✓✓✓Stable
Conversation state machine✓✓PartialBeta
Modal / Dialog✓✓✓Stable
Bottom sheet✓✓✓Stable
List row✓✓✓Stable
Data table✓✓✓Stable
Card✓✓✓Stable
Form✓✓✓Stable
Change-log feed✓✓PartialBeta

Reading the table

  • Stable — passed full review · safe to consume in product · changes are additive only.
  • Beta — visual and interaction spec is settled but a11y or cross-product review is still pending. Safe to consume; expect minor refinements before the next edition.
  • Experimental — exploratory · no consumer expectation · may be removed.
  • Deprecated — scheduled for removal; do not consume.
  • Partial in A11y means the audit ran but at least one note is open. Linked from the component's metadata strip.

Figma bundle

The Figma library mirrors the atomic IA. Tokens published as Figma variables from tokens.json in DTCG 2025.10 format.

tokens.json · DTCG 2025.10

{
  "$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)."
}

figma-components.md

Aira DS v1.0 — Figma Component Specs

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).

---

Tier 1 — Policy-critical components

---

1. ChatBubble

Component name: chat/Bubble Selector: .msg

Variants (Figma variant property):

  • role: user · ai · system · error
  • agent (ai only): config · qa · analytics · support · coder
  • state: default · hover · selected
  • density: comfortable · compact

Properties (Figma component properties):

  • Text → message body (instance swap, supports markdown)
  • Show meta → boolean (default true for ai/error)
  • Show actions → boolean
  • Timestamp → text

Anatomy (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.

---

2. AILabel

Component name: ai/Label Selector: .ai-label

Variants:

  • size: inline (pill, 11/16 overline) · block (label, 12/16, full disclosure copy)

Anatomy:

  • Dot 6×6, fill ai.accent, 6px gap.
  • Text: ai.accent on ai.accentSurface, border ai.accentBorder.
  • Padding: inline 2/8, block 8/12. Radius pill for inline, md for block.

Mandatory use: on every AI-authored output (chat replies, generated configs, generated SQL, drafted copy).

---

3. ApprovalCard

Component name: approval/Card Selector: .approval

Variants:

  • status: draft · awaiting · approved · live · rejected
  • risk: 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.

---

4. CitationChip

Component name: citation/Chip Selector: .citation

Variants:

  • mode: inline · listed
  • state: default · hover · focus

Properties:

  • Number (overline)
  • Title (label-sm)
  • Show snippet on hover (boolean — pairs with Popover)

Anatomy:

  • Padding 4/8, gap 6, radius sm.
  • Bg citation.bg, text citation.text, border citation.border.
  • Inline variant sits within prose; listed variant lives in a Sources block under the answer.

A11y: real anchor (<a href>) — never div onclick. Hover popover surfaces the cited snippet.

---

5. CodeBlock

Component name: code/Block Selector: .code-block

Variants:

  • lang: sql · javascript · python · yaml · text
  • state: default · collapsed
  • ai-labelled: true · false

Properties:

  • Code (text, monospace)
  • Lang (badge text)
  • Show Copy (boolean)

Anatomy:

  • Container: radius md, border code.border, bg code.bg.
  • Header bar: 8px padding, bg surfaceAlt, border-bottom. Lang overline (tertiary) · AILabel (optional) · spacer · Copy (tertiary button).
  • Body: padding 12/16, font code, color code.fg. Syntax token colors per v2-additions..syntax..

Collapsed: cap at 240px height with a "Show all" tertiary button below.

---

6. InlineDiff

Component name: diff/Inline Selector: .diff

Variants:

  • density: comfortable · compact
  • show-line-numbers: true · false

Anatomy (per line: 36px gutter + 1fr body):

  • Gutter: bg diff.gutterBg, fg diff.gutterFg, font overline, right-aligned.
  • Add line: bg diff.addBg, fg diff.addFg, gutter prefixed with +.
  • Remove line: bg diff.removeBg, fg diff.removeFg, gutter prefixed with −.
  • Context line: bg diff.contextBg, fg diff.contextFg.

Always pair sign + color — the + / − marker is the primary signal.

---

7. AgentActivity

Component name: activity/Row and activity/List Selector: .activity, .activity-list

Variants (Row):

  • state: thinking · streaming · stopped · error

Variants (List item):

  • state: pending · active · done · failed

Anatomy (Row):

  • Padding 8/12, radius md, bg state.aiThinking, border state.aiThinking, text state.aiThinking.
  • 14px spinner left, step text (medium), inline detail (tertiary).

Anatomy (List):

  • Each item: 20px status dot (pending: strong border, active: brand pulse, done: green fill, failed: red fill) + step name (label-md) + duration time (caption, right).

A11y: wrap in role="status" aria-live="polite". Honor prefers-reduced-motion (spinner stops, surface stays).

---

8. FeedbackControl

Component name: feedback/Control Selector: .feedback

Variants:

  • state: default · up-selected · down-selected · disabled
  • size: sm (24) · md (28) · lg (32)

Anatomy:

  • Inline group, 4px gap. Each button 28×28, radius sm, border default.
  • Up selected: bg statusSuccess, border green.300.
  • Down selected: bg 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.

---

9. Modal

Component name: overlay/Modal Selector: .modal

Variants:

  • intent: confirmation · destructive · info · form
  • size: sm (440) · md (560) · lg (720)

Properties:

  • Title (h3, primary)
  • Body slot (instance swap)
  • Primary CTA (button)
  • Secondary CTA (button)

Anatomy:

  • Width per size, max 90vw. Radius xl (16). Shadow lg.
  • Header: padding 20, border-bottom subtle.
  • Body: padding 20, body-md text.
  • Footer: padding 12/20, bg 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.

---

10. BottomSheet

Component name: overlay/BottomSheet Selector: .sheet

Variants:

  • size: peek (25%) · default (50%) · full (95%)
  • dismissible: true · false (drag handle visibility & scrim click)

Anatomy:

  • Width 100%, anchored bottom. Radius top 20px. Shadow lg. Bg surface.
  • Padding: 12/16/20 with bottom = 20 + safe.bottom.
  • Drag handle: 36×4, radius 999, bg border.strong, centered, 12px below the top edge.
  • Title h4 + body-sm description + actions row (right-aligned).

A11y: same as Modal. Handle is decorative — always include explicit Cancel / Apply buttons.

---

Tier 2 — General component coverage

---

11. Tabs

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.

---

12. Select

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.

---

13. Combobox

Component name: form/Combobox Selector: .combo

Variants: state: closed · open · loading · empty. Anatomy:

  • Input identical to Select sans chevron.
  • Menu (popover): bg surface, border default, radius md, shadow md, max-height 240, scrollable.
  • Item: 32px, padding 8/12, hover bg 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.

---

14. Toggle

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.

---

15. Checkbox

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.

---

16. RadioGroup

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.

---

17. Tooltip & Popover

Component name: overlay/Tooltip, overlay/Popover Selector: .tip, .popover

Tooltip:

  • Bg slate.900 (light) / slate.100 (dark). Text white/slate-900. Padding 6/8. Radius sm. Shadow md. Max-width 220.
  • Shown on hover AND focus.

Popover:

  • Bg surface, border default, radius md, shadow md, padding 12/16. Max-width 320.
  • Includes title (h4) + body (body-sm) + optional action row.
  • Behaves like a small dialog (role="dialog").

---

18. Avatar

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.coder

Anatomy: circle, font label-md (sm: label-sm, lg: h4). user: bg brand, text onBrand. ai: gradient aira-gradient. agent.: bg matching agent. token.

---

19. ListRow

Component name: list/Row Selector: .list-row

Variants:

  • density: comfortable (min-height 44) · dense (min-height 36)
  • state: default · hover · selected · disabled

Anatomy: 3-column grid (auto · 1fr · auto), 12px gap, padding 12/16. Border-bottom subtle.

  • Leading slot (avatar/icon)
  • Content: title (label-md) over subtitle (caption, tertiary)
  • Trailing slot (timestamp / count / chevron)

A11y: entire row is one interactive surface (<a> or <button>); never split.

---

20. DataTable

Component name: data/Table Selector: .dt

Variants (cell): align: start · end. type: text · number · delta. Anatomy:

  • Header row: bg surfaceAlt, label-sm, text primary.
  • Row: body-sm, border-bottom tableDivider. Hover bg hover.
  • Number cells: tabular-nums, right-aligned.
  • Delta cells: positive uses delta.positive (green) with +; negative uses delta.negative (orange, NOT red) with −.

---

21. Stepper & Progress

Component names: progress/Stepper, progress/Bar Selectors: .stepper, .progress

Stepper variants (per step): state: pending · active · done · failed. Anatomy:

  • Each step: horizontal auto-layout, padding 8/12, flex 1, 2px bottom border.
  • Number circle 22×22 with label-sm digit. done: green ✓. active: brand. pending: slate.

Progress bar:

  • Height 6, radius 999, bg sunken. Fill brand, animated to width.
  • A11y: role="progressbar" + aria-valuemin/max/now.

---

Extras

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.

---

Naming convention

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.

code-connect.json

{
  "$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."
  ]
}

Deferred

Work consciously left out of Aira Design System v1.1. Each item has a trigger condition for re-opening.

ItemWhy deferredRe-open when
Cmd+K command palette + slash commandsThe command surface isn't designed yet (which commands, namespaced how, scoped to which agent).Product defines the command vocabulary.
Multi-brand theming hooksThe 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 tokensThe 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 pagesThe earlier standalone pages were dropped; the rule lives in tokens.css §7a but isn't documented.Next edition.

Risk register

  • Base UI pre-1.0 churn. @base-ui/react APIs may still shift; pin the version and isolate changes behind the AiraX wrappers.
  • Chart performance. recharts (SVG) degrades past a few thousand points; drop to a d3 + canvas renderer for high-cardinality series.
  • Virtualization vs. a11y. @tanstack/react-virtual removes off-screen rows from the DOM; keep aria-rowcount/aria-setsize accurate so assistive tech still reads totals.
  • Token drift. Hard-coded values bypass the Tailwind theme; the lint rules below are the guardrail.

Lint rules

  • Tokens only — no raw hex, spacing, or radius literals; values must resolve from the Tailwind theme / CSS variables.
  • Controlled props — use value + onValueChange, not bare onChange handlers.
  • Icon labels — an icon-only control without an aria-label fails lint.
  • Dates via date-fns — no new Date() math or hand-built format strings in components.
  • Virtualize long lists — a scrollable list rendering > 50 rows without @tanstack/react-virtual fails lint.

Language strategy

  • Sentence case everywhere; one aiRA voice; refusal grammar is "can't / can / next".
  • No concatenated strings — full ICU messages with named variables, so translators get whole sentences.
  • Locale-aware dates and numbers via date-fns locales; currency/grouping resolve from the brand locale (never a hard-coded prefix).
  • RTL via dir + CSS logical properties (the system already ships LTR/RTL); no left/right hard-coding.

Changelog

StableUpdated Owner · Punya Shetty

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.

v1.1 · 2026-06-17
Selection card·Changed
Replaced the confusing Skip button on the "Something else" row with a disclosure chevron. The row expands a free-text composer, so it now reads as an expander (the chevron rotates when open) instead of cramming "answer differently" and "abandon" into one row. Skipping / dismissing the card stays on the header × and Esc.
Color·Fixed
Synced the shell surfaces to pixel-sampled values from the product screenshots (sampled, not eyeballed): chat panel = 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.
Selection card·Fixed
Pinned the dismiss (×) button to the card's right edge in every variant — the title now absorbs the header's free space, so the close stays flush-right (and top-aligned) whether or not the header carries a step counter. Previously it hugged the title on the free-text variants.
Color·Added
Documented the developer's full semantic token set for Storybook parity — grouped exactly as it ships in code: BACKGROUND, TEXT, and BORDER, each token with its light & dark value, in Color. Specified the tokens that were previously undocumented: backgrounds --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.
Color·Added
Specified the product shell's light & dark surfaces and on-surface text — previously only implicit in the tokens. The chat panel and artifact panel sit on the canvas (#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.
Selection card·Added
Detailed the free-text behaviour: "Something else" now opens a real, focusable composer the user can type a full paragraph into. It starts at two lines and auto-grows to ~7 lines, then scrolls inside the field; Send enables on non-empty text; ⏎ sends and Shift+⏎ inserts a line. Specified four variants — Pick one, Free text, Free text · filled, and Open-ended (no options) — plus new props (allowFreeText, freeTextPlaceholder, maxRows, submitOnEnter, onSubmitText) and a textarea row in the interaction matrix.
v1.1 · 2026-06-07
Logo & marks·Fixed
Replaced the aiRA sparkle with the official brand SVG (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 / brand mark·Changed
aiRA no longer has an avatar. The sparkle is documented exclusively as the brand mark (a logo element) — removed the "aiRA avatar" demo and its Do/Don't from Avatar, dropped the unused .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.
Iconography·Added
Added the side-panel icon (PanelLeft) from lucide-react to the icon library.
Button·Fixed
Disabled buttons no longer pick up hover/active styling. The disabled state now also covers :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).
Iconography·Changed
Migrated the icon reference to the developer's icon module — icons are re-exported from 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).
Tokens·Changed
Adopted the developer's --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.
Accessibility·Fixed
Re-ran a token-by-token contrast audit and corrected overstated claims: fixed the ratios in both Accessibility tables, relabelled the dark primary button AAA → AA (its real value is 5.7 : 1), and softened "AA+ / verified token-by-token" to an accurate AA floor. Surfaced text-placeholder (2.5 : 1) as an intentional sub-AA hint that relies on the field label.
Figma status·Fixed
Removed Figma overclaims: Component status now reads "a Figma spec exists (the file isn't built yet)" instead of "a 1:1 Figma library component exists", and the cover says the system is "structured to map onto a Figma library" rather than implying one exists.
Developer sync·Changed
Synced the system to the developer Storybook. Every component now shows its dev name (AiraButton, AiraConversation, AiraDataTable…) plus an Engineering handoff block — the libraries it's built with and a React/Tailwind snippet.
Conventions·Changed
Rewrote Conventions around the shipped @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.
Get started·Added
Added AI fluency fixes and Tokens & MCP sections, plus About → Stack / Non-goals / How to contribute, matching the developer reference.
Deferred·Changed
Removed "React component library" (it has shipped); added a Risk register, Lint rules, and Language strategy.
Versioning·Changed
Bumped the design system to v1.1 across the title, cover, About, README, tokens, and Figma bundle metadata.
Naming·Changed
Renamed the system to Aira Design System (with Aira/aira- code prefixes) to align with the developed design system. The assistant brand wordmark aiRA and the Logo & marks rules are intentionally unchanged.
Documentation·Added
Added a Directly Responsible Individuals (DRI) section — Design: Punya Shetty · Development: Keshav Agrawal & Vaibhav V Bhat · Product: Harsh Deo.
Cross-cutting rules·Added
Added a distinct rule: there is no coder agent — no coder-agent tokens, badges, avatars, or labels; code output comes from the one aiRA.
Conventions·Removed
Removed the "aiRA is a B2B power-user" framing from the command-palette note.
Conventions·Changed
Keyboard shortcuts now document both Mac and Windows (e.g. ⌘K / Ctrl+K, ⌘/ / Ctrl+/) — see the new Mac & Windows shortcuts table in Keyboard (kbd).
Conventions·Added
Added a Developer alignment · multi-brand note: developer-facing tokens, names, and the override path follow the first iteration of the shared developed (Aira) system.
Figma bundle·Changed
Relabelled the Figma component specs from v2 to v1.0 and added a note that a Figma file has not been created for these components yet.
Email·Changed
Copyright now reads as two sentences separated by a full stop ("© 2026 Capillary Technologies. All rights reserved.") and is centre-aligned in every template.
Iconography·Changed
Tightened icon definitions: plus → upload; thumbs-up = like / thumbs-down = dislike (dislike no longer opens a reason picker); pencil = edit / rename; cross = dismiss / remove; download (dropped "save out"); link (dropped "citation chip"); eye = show / view, eye-off = hide (dropped "password"); pause = pause, play = play / resume; life-buoy = support in the action row below aiRA's responses.
Iconography·Changed
Redesigned the sort icon — replaced the up/down chevrons with two opposing arrows (down on the left, up on the right), per the supplied reference. Applied to the Data table column headers; colour + aria-sort still carry the active direction.
Icon button·Changed
The Stop icon in the generating state now matches the default — removed the primary-indigo fill background.
Chat message·Changed
Removed avatars from the chat — no user avatar, no aiRA sparkle/avatar on replies; aiRA's reply free-flows as plain text. The cached-query-plan system line is now aligned flush with the reply (no indent), and the inline alert block is smaller / less prominent (error styling retained).
Conversation state machine·Changed
Idle state reworked: the input is the live region with a blinking caret so users can type immediately without clicking; Send stays disabled. Transition rules rewritten as a clear from → trigger → to table.
List row·Fixed
Fixed the "New chat" (+) button in the sessions rail — clearer hover state, and clicking it now opens a new chat (prepends a fresh active session).
Data viz·Fixed
Corrected the colour-vision stat to the general population ("about 1 in 20 people"), removing gender-specific phrasing.
Data viz·Changed
Email and Push now use clearly different hues (teal vs indigo) for higher contrast and easier differentiation.
Data viz·Fixed
Fixed the histogram "number of days" axis — 0 / 30 / 60 / 90+ ticks are now evenly spaced and correctly positioned.
Data viz·Added
Added a dynamic numeric-formatting rule — currency, grouping, and decimals resolve from the brand's locale/currency at render time; no hard-coded prefixes.
Loading·Changed
Loading skeletons now blend a subtle touch of the aiRA brand gradient into the grey shimmer.
Approval card·Added
Noted that status variants are mainly for configurations.
Approval card·Changed
Default actions are now exactly Edit and Create (Reject removed); framing copy updated to "aiRA generated this promotion from your prompt, review the diff and edit or create."
Approval card·Changed
High-risk Confirm button changed from red to the primary indigo colour.
Modal·Added
Added validation pop-up variants — including "Are you sure you want to go back a step? There will be changes in another step."
Code block·Removed
Removed the Explain action (not a user action) from the code block and its Figma spec.
Inline diff·Changed
Rewrote the inline diff explanation — before → after framing, a legend, and a "how to read it" guide.
Mobile·Added
Added the missing voice input control to the mobile composer.
Action row·Changed
Brought the Action row in line with the Feedback control — thumbs are now like / dislike (dislike no longer opens a reason picker), and the lede notes Support opens help.
Documentation·Removed
Removed third-party product names — the chat message and list row no longer reference other products by name.
Logo & marks·Changed
Applied a 5 px border-radius to the aiRA sparkle logomark at every render size — softens the optical edge while keeping the geometry locked.
Documentation·Added
Introduced this Changelog section (the published v1.0 had none), tagging every change Added / Changed / Removed / Fixed.
Approval card·Removed
Removed the Multi-brand override path subsection; the work is tracked under Deferred · Multi-brand theming hooks.
Action row·Changed
Copy and Support are now icon-only (labels moved to aria-label), and all four action icons are sized consistently at 18 px.
Build & structure·Changed
Split the single file into editable source — tokens/tokens.json, css/, html/components/ — with scripts/generate.js (tokens → CSS) and scripts/bundle.js (→ a self-contained dist/index.html).
v1.0 · 2026-05-31
Initial release·Added
First Edition — atomic IA (Atoms → Molecules → Organisms → Templates), a light + dark token system, the component reference library, voice & tone, and the Figma bundle. Published as aiRADS v1.0.

How this changelog works

  • One entry per change. A change is anything that affects a token value, a component contract, a voice rule, or a documented pattern. Bumping a build number is not a change.
  • Tag every entry. Use Added, Changed, Removed, or Fixed. No untagged entries.
  • Group by edition. Day headers carry the edition label and the date the edition shipped. Multiple changes inside the same edition share a header.
  • Plain language summaries. Say what changed and why a consumer should care. Never paste a diff or a commit message verbatim.
  • Link out for detail. Link to the component, token, or Deferred row a change touches, so readers can follow the breadcrumb without leaving the document.

Do / don't

Do

Write the summary so a designer or engineer who skipped this edition can act on it without opening anything else.

Don't

Backdate or rewrite an entry once an edition has shipped. Add a new Fixed entry instead.

Do

List removals explicitly — including the reason and where the work moved to (e.g. Deferred).

Don't

Bury removals inside a "Changed" entry. Removed tokens and removed components get their own line.

Directly Responsible Individuals (DRI)

The people responsible for the Aira Design System — the points of contact for design, development, and product decisions.

AreaResponsible person
DesignPunya Shetty
DevelopmentKeshav Agrawal · Vaibhav V Bhat
ProductHarsh Deo