Locara Components Library — Design Decisions and Provenance
What this is: The design log for @locara/components — what we built, where each pattern came from, and why we made the architectural choices we did. Pairs with the live showcase at /components and the canonical spec at /spec/11-components.md.
Why it matters: A great component library is a translation of existing-great-product UX into reusable primitives. To do that honestly, we need to acknowledge which products minted each pattern, what we kept, what we changed, and why.
Most relevant to Locara: Pairs with shadcn-ui.md (the distribution model), v0-community-design-systems.md (the AI-friendly source-distribution lineage), design-tokens.md (the tokens layer), radix-headless-primitives.md (accessibility patterns).
Architecture — why these specific choices
Stack: React + TypeScript + plain CSS (no Tailwind)
The original /spec/11-components.md called for Tailwind. We ended up shipping plain CSS instead. Reasons:
- No build-step required. A Locara app can
import '@locara/components/styles.css'once and every component looks right. No Tailwind config, no PostCSS pipeline, no purge step. Apps that already use Tailwind can still consume the components; thelc-*class prefix avoids collisions. - Design tokens flow through CSS variables. Apps override
--accent,--bg,--fg-mutedat:rootand every component picks it up. With Tailwind, theming requires extending the config. - AI-friendly without the noise. Cursor / Claude read a
.cssfile faster than a long string of utility classes. shadcn pioneered Tailwind in this space because Radix is unstyled; we lean on plain CSS because we own our tokens. - Hand-authored CSS reads like a design document. Each component’s
.cssshows the intentional design rules (spacing, motion, hover states) instead of being implicit in utility-class strings.
shadcn-style copy-in via locara add <name> still works the same way: the CLI fetches both .tsx and .css into the app’s source tree.
Distribution: shadcn-style copy-in and package import
We’re not forcing one model. The package @locara/components re-exports everything so the framework’s own apps (and the website showcase) can import { Chat } from '@locara/components'. Outside developers can either:
- Copy in (the recommended path):
locara add chat-composerpulls the source into their repo. - Import directly:
import { ChatComposer } from '@locara/components'if they want updates to flow.
The copy-in path is the contract we promise to keep stable. The direct-import path is a convenience.
Token system: CSS variables, dark-by-default, single override point
Tokens live in packages/components/src/theme.css. The whole library reads from them. Apps override any token at :root (or scope-level for theme switching). Hot-reload of token values works in dev without rebuilding anything.
Inspired by Linear’s stated practice: a small token set, enforced by judgment rather than by a sprawling design-system org.
Component provenance — who minted each pattern
| Component | Provenance | What we kept | What we changed |
|---|---|---|---|
| ChatComposer | Cursor + Claude composers | Inset action icons, auto-grow, ⌘↵, attachments-above-textarea | Inlined model picker + mode tabs in the toolbar, not behind a + menu |
| ModelSelector | Cursor model picker, OpenRouter | Name + capability badges; sparse, no per-token cost | Added per-row download flow (the user-facing differentiator for local AI) |
| ModelDownloader | Ollama model pull, Hugging Face hub UI | Hash visibility (content-addressed identity), ETA, throughput | Added a verifying state (Locara content-addresses; verification is real) |
| ListeningPill | Granola live-meeting pill | Floating + draggable, audio bars, REC indicator | Added explicit floating prop so it works both inside and outside a window |
| ListeningSurface | Granola + Superwhisper + ChatGPT voice mode | Orb-or-waves switch, partial+final transcript display, state machine | Combined controls into one panel rather than scattered UI |
| VoiceOrb | ChatGPT voice mode, Claude voice, Siri | Pulsating gradient orb, amplitude-reactive | State-machine API (idle/listening/thinking/speaking/error) — most products only model one or two states |
| WaveformBars | Apple Voice Memos, system equalizers | Bar-count, height, amplitude config | Two-mode (static idle vs live) so it doubles as a “ready” indicator |
| CitationChip | Perplexity inline [1][2] pills | Numeric & badge variants, baseline-align, hover-sync | Kept it transport-agnostic (link or onClick — useful for in-app transcript scrolling) |
| SourceCard | Perplexity sources rail | Favicon + number + title + domain + snippet | Letter fallback (no favicon fetching by default) |
| ToolCallCard | Cursor tool cards, Claude tool use | Collapsible, status badge, approve/reject | Made the requiresApproval state explicit — local-AI runs untrusted code and the user gate matters |
| Reasoning | Claude reasoning blocks, o1/o3, DeepSeek-R1 | Collapsed by default, sparkle + duration | ”Thought for Ns” summary; ours surfaces duration when collapsed too |
| ThinkingIndicator | Claude shimmer, iMessage dots, Linear pulse | All three variants in one component | A single variant prop instead of three components |
| StreamingText | ChatGPT/Claude caret | Blinking caret at the tail while streaming | Tiny wrapper rather than a coupled message component |
| ContextMeter | Cursor token meter, Workbench | Used/total, warn/danger thresholds | Bar + pill variants — pill works in compact toolbars |
| ModeSwitcher | Cursor Ask/Edit/Agent | Pill tabs, shortcut hints | Default modes provided, but the consumer can pass any Mode[] |
| AttachmentChip | Claude attachment chip, ChatGPT pill | Icon + name + size + remove | Per-kind icons (image, audio, video, pdf, doc, code, file) inline |
| StatusPill | Linear status pills, Notion property pills | Colored dot + label, fully rounded | Added a live tone + pulse for “Listening / Recording” semantics |
| EmptyStateCards | ChatGPT, Claude, Perplexity blank states | Eyebrow + title + 3-4 prompt cards | Optional hint line per card for app-specific context |
| FollowupRow | Perplexity follow-ups, Claude suggestions | Arrow-prefixed ghost chips, label-above | Chips wrap rather than scroll horizontally |
| AcceptReject | Notion AI inline block, Granola enhanced notes | Discard / Retry / Accept ribbon | Optional label slot for context (“Inserted 3 sentences”) |
| SlashMenu | Notion slash menu | Caret-anchored, query filtering, keyboard nav | Made anchor optional so the menu can render anywhere |
| CommandPalette | Linear ⌘K, Raycast, VSCode quick-open | Fuzzy search, sticky group headers, kbd shortcuts right-aligned | Items passed flat; grouping derived from group field — no nested structure |
| HoverActionRow | Linear issue rows, GitHub PR rows | Clean rest, hover-revealed actions | Pure CSS — no state library, no JS event coupling |
| AIText | Granola gray-AI / black-user | Two modes; whole-block + mixed-pieces | flashOnChange opt-in for transition highlights |
The takeaway: every “novel” Locara component is a translation of a pattern that already exists in a recognized-great product. We don’t try to invent UX from scratch; we identify the best version of each pattern and ship a single canonical implementation.
Spacing, motion, and color — the rules we enforce
From the AI design research (see docs/adr/0006-shared-folder-ipc.md cross-reference for the broader design audit):
- 8px grid is religion. Spacing scale: 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 / 48 / 64. No “in-between” values.
- Type scale: 11/12/13/14/16/20/24/32. Most UI lives at 13–14px. Display headings only at 24+.
- Border-radius scale: 4 / 6 / 8 / 10 / 14 / 9999. Inputs/buttons 6–8px; cards 8–14px; pills fully rounded.
- Motion durations:
- Micro (hover, button states, dropdown open): 80–140ms with
cubic-bezier(0.2, 0, 0, 1)(ease-out). - Panel transitions: 140–240ms.
- Streaming text: ~30–60 tokens/sec visible cadence (consumer-driven).
- Shimmer cadence: 1.5–2.5s loop, 60–80% opacity range.
- No bouncy springs on functional UI. Reserve for celebration only.
- Micro (hover, button states, dropdown open): 80–140ms with
- Hairlines: 1px, low-alpha.
rgba(0,0,0,0.06)light,rgba(255,255,255,0.08)dark — never solid. - One accent. A single honey-gold by default (
#d9a468family). Never two competing colors in the same view. - Hover backgrounds: 4–6% delta. Never bright color shifts — calm and subtle.
These rules don’t live in a doc nobody reads. They live in theme.css as named tokens, and every .css file references them. The “rules” enforce themselves.
Patterns we deliberately did NOT build
A few patterns showed up in the research that we considered and rejected:
- Per-token cost meter. OpenRouter shows USD per response. We don’t — Locara is local-first; cost is electricity and time, not API spend. A
ContextMetershowing token count is the better fit. - Provider logos in the model picker. t3.chat shows OpenAI/Anthropic/Gemini logos. Locara apps are local-only; the “provider” is the user’s own machine. We support a
providerstring (“Local” / “Apple” / etc.) but no logo grid. - Branch / fork conversation icon. t3.chat lets users fork a chat at any message. We considered this but decided not to ship without a real backing implementation in the SDK first.
- Artifact / preview panel. Claude artifacts is great but it’s a structural decision (the chat reflows to make room) — the consumer’s app shell needs to handle it. We didn’t ship a one-size
<ArtifactPanel>because it would falsely imply the rest of the app is artifact-aware. Apps can compose usingSandboxFrame+Card. - Image-generation result block. Locara doesn’t ship text-to-image as a v1 modality. When it does, we’ll add
<GenerationResult>. Not before. - Custom GPTs picker. ChatGPT-style
@-mention of “personas” or specialized assistants. Locara apps are typically single-purpose; this is a multi-tenant pattern that doesn’t fit yet.
Specific learnings for Locara
- Component naming should reference provenance. When the consumer reads
ListeningPill, they should think “Granola.” When they readModeSwitcher, “Cursor.” Naming is documentation. We deliberately preserved that mental-model mapping in our names. - Polish lives in the small components.
StatusPill,Kbd,CitationChip,AttachmentChipare tiny. They’re also the components that get used the most. The library’s perceived quality is set by the quality of its smallest pieces. - State machines beat boolean props.
VoiceOrbhas astateenum, notisListening/isThinkingbooleans. Mutually-exclusive states are clearer as a single enum and prevent invalid combinations. - Approval gating is a first-class affordance.
ToolCallCard.requiresApprovalis a load-bearing prop because Locara’s whole pitch is that capability is kernel-enforced. The UI must surface that visibly. - The composite components are where polish compounds.
ChatComposerandListeningSurfaceare not just “Composer + ModelSelector” — they’re hand-tuned compositions where the spacing between sub-parts is what makes them feel right. Apps that use the composites get the polish for free; apps that wire their own composer have to recreate the tuning. - The showcase is part of the product. A library without live previews is dead-on-arrival. Building the website showcase in parallel with the components forces the API to be demonstrable — if a component can’t be shown well in a static demo, its API is probably wrong.
- Static-HTML demos are good enough. We don’t need React in the showcase. The CSS does the visual work; hand-authored HTML matches what the React component renders. Saves a build dependency, makes the showcase faster to navigate.
- Don’t reinvent — translate. Every pattern in this library exists somewhere. The library’s job is to translate the best version of each pattern into a clean React API. Inventiveness here is a smell — if a Locara component looks unlike anything that already ships in a great product, ask why.
References
Products studied (primary inspiration):
- Granola —
https://granola.ai - Cursor —
https://cursor.com - Claude.ai —
https://claude.ai - ChatGPT —
https://chatgpt.com - Perplexity —
https://perplexity.ai - Linear —
https://linear.app - Raycast —
https://raycast.com - v0.dev —
https://v0.dev - Notion AI —
https://notion.com/product/ai - Anthropic Console —
https://console.anthropic.com - t3.chat — Theo’s multi-provider client
- OpenRouter —
https://openrouter.ai - MacWhisper —
https://macwhisper.net - Superwhisper —
https://superwhisper.com
Designers and writers who shaped the practice:
- Karri Saarinen (Linear) — system thinking with minimal tokens
- Rauno Freiberg (Vercel) — “Devouring Details” + Web Interface Guidelines
- Brian Lovin (Notion AI) — App Dissection
- Cultured Code (Things 3) — purposeful animation
- The Anthropic design team — editorial typography in AI products
Locara cross-references:
shadcn-ui.md— copy-in source distribution modelv0-community-design-systems.md— AI-friendly sourcedesign-tokens.md— token-first themingradix-headless-primitives.md— accessibility patternsmac-llm-optimization.md— streaming UI guidance/spec/11-components.md— canonical spec/docs/adr/0005-shadcn-style-components.md— the distribution-model ADR