Locara

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:

  1. 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; the lc-* class prefix avoids collisions.
  2. Design tokens flow through CSS variables. Apps override --accent, --bg, --fg-muted at :root and every component picks it up. With Tailwind, theming requires extending the config.
  3. AI-friendly without the noise. Cursor / Claude read a .css file 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.
  4. Hand-authored CSS reads like a design document. Each component’s .css shows 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:

  1. Copy in (the recommended path): locara add chat-composer pulls the source into their repo.
  2. 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

ComponentProvenanceWhat we keptWhat we changed
ChatComposerCursor + Claude composersInset action icons, auto-grow, ⌘↵, attachments-above-textareaInlined model picker + mode tabs in the toolbar, not behind a + menu
ModelSelectorCursor model picker, OpenRouterName + capability badges; sparse, no per-token costAdded per-row download flow (the user-facing differentiator for local AI)
ModelDownloaderOllama model pull, Hugging Face hub UIHash visibility (content-addressed identity), ETA, throughputAdded a verifying state (Locara content-addresses; verification is real)
ListeningPillGranola live-meeting pillFloating + draggable, audio bars, REC indicatorAdded explicit floating prop so it works both inside and outside a window
ListeningSurfaceGranola + Superwhisper + ChatGPT voice modeOrb-or-waves switch, partial+final transcript display, state machineCombined controls into one panel rather than scattered UI
VoiceOrbChatGPT voice mode, Claude voice, SiriPulsating gradient orb, amplitude-reactiveState-machine API (idle/listening/thinking/speaking/error) — most products only model one or two states
WaveformBarsApple Voice Memos, system equalizersBar-count, height, amplitude configTwo-mode (static idle vs live) so it doubles as a “ready” indicator
CitationChipPerplexity inline [1][2] pillsNumeric & badge variants, baseline-align, hover-syncKept it transport-agnostic (link or onClick — useful for in-app transcript scrolling)
SourceCardPerplexity sources railFavicon + number + title + domain + snippetLetter fallback (no favicon fetching by default)
ToolCallCardCursor tool cards, Claude tool useCollapsible, status badge, approve/rejectMade the requiresApproval state explicit — local-AI runs untrusted code and the user gate matters
ReasoningClaude reasoning blocks, o1/o3, DeepSeek-R1Collapsed by default, sparkle + duration”Thought for Ns” summary; ours surfaces duration when collapsed too
ThinkingIndicatorClaude shimmer, iMessage dots, Linear pulseAll three variants in one componentA single variant prop instead of three components
StreamingTextChatGPT/Claude caretBlinking caret at the tail while streamingTiny wrapper rather than a coupled message component
ContextMeterCursor token meter, WorkbenchUsed/total, warn/danger thresholdsBar + pill variants — pill works in compact toolbars
ModeSwitcherCursor Ask/Edit/AgentPill tabs, shortcut hintsDefault modes provided, but the consumer can pass any Mode[]
AttachmentChipClaude attachment chip, ChatGPT pillIcon + name + size + removePer-kind icons (image, audio, video, pdf, doc, code, file) inline
StatusPillLinear status pills, Notion property pillsColored dot + label, fully roundedAdded a live tone + pulse for “Listening / Recording” semantics
EmptyStateCardsChatGPT, Claude, Perplexity blank statesEyebrow + title + 3-4 prompt cardsOptional hint line per card for app-specific context
FollowupRowPerplexity follow-ups, Claude suggestionsArrow-prefixed ghost chips, label-aboveChips wrap rather than scroll horizontally
AcceptRejectNotion AI inline block, Granola enhanced notesDiscard / Retry / Accept ribbonOptional label slot for context (“Inserted 3 sentences”)
SlashMenuNotion slash menuCaret-anchored, query filtering, keyboard navMade anchor optional so the menu can render anywhere
CommandPaletteLinear ⌘K, Raycast, VSCode quick-openFuzzy search, sticky group headers, kbd shortcuts right-alignedItems passed flat; grouping derived from group field — no nested structure
HoverActionRowLinear issue rows, GitHub PR rowsClean rest, hover-revealed actionsPure CSS — no state library, no JS event coupling
AITextGranola gray-AI / black-userTwo modes; whole-block + mixed-piecesflashOnChange 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.
  • 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 (#d9a468 family). 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:

  1. 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 ContextMeter showing token count is the better fit.
  2. 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 provider string (“Local” / “Apple” / etc.) but no logo grid.
  3. 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.
  4. 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 using SandboxFrame + Card.
  5. Image-generation result block. Locara doesn’t ship text-to-image as a v1 modality. When it does, we’ll add <GenerationResult>. Not before.
  6. 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

  1. Component naming should reference provenance. When the consumer reads ListeningPill, they should think “Granola.” When they read ModeSwitcher, “Cursor.” Naming is documentation. We deliberately preserved that mental-model mapping in our names.
  2. Polish lives in the small components. StatusPill, Kbd, CitationChip, AttachmentChip are 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.
  3. State machines beat boolean props. VoiceOrb has a state enum, not isListening/isThinking booleans. Mutually-exclusive states are clearer as a single enum and prevent invalid combinations.
  4. Approval gating is a first-class affordance. ToolCallCard.requiresApproval is a load-bearing prop because Locara’s whole pitch is that capability is kernel-enforced. The UI must surface that visibly.
  5. The composite components are where polish compounds. ChatComposer and ListeningSurface are 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.
  6. 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.
  7. 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.
  8. 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: