Strata components (@layers/ui)

Source of truth for per-component contracts: the strata site — every component has a page at strata.layers.com/components/<slug> (source: apps/strata/src/component-docs/) with a live demo, when-to-use, and the house rules that bind it. Coverage is ENFORCED: a build gate (apps/strata/scripts/check-component-coverage.mjs) fails the site build for any packages/ui component without a page. This doc covers the package mechanics and the cross-consumer rules.

Package mechanics

  • Source-consumed. @layers/ui has no build step — consumers transpile src/ directly and Tailwind scans it (every consumer stylesheet needs @source ".../packages/ui/src"). Changes are live in every app on save.
  • Subpath imports preferred: import { Button } from '@layers/ui/button'. The barrel (@layers/ui) exists but subpaths keep bundles honest.
  • CSS that a component needs beyond Tailwind lives in packages/ui/src/css/<component>.css (media-card, media-player, stat-card, content-card) — new components should be self-contained Tailwind; a companion CSS file is only for keyframes or pseudo-selectors Tailwind can’t express.
  • Primitives are shadcn-derived (base-maia style) on @base-ui/react (Base UI). Radix is fully retired from this package: triggers take a render prop (never asChild), overlay anatomy is Portal > Positioner > Popup (Backdrop for scrims), and state hooks are data-open/data-closed/data-popup-open. Per-component migration notes live in .migration/*.md.

Inventory (by kind)

shadcn primitives (stock — structure per the shadcn website, never modified): Alert, AlertDialog, Avatar, Badge, Bubble, Button, ButtonGroup, Calendar, Card, Checkbox, Combobox, Command, Dialog, DropdownMenu, Field, Form, Input, InputGroup, InputOTP, Label, Marker, Message, MessageScroller, Popover, Progress, RadioGroup, Select, Separator, Skeleton, Slider, Switch, Tabs, Textarea, Toggle, ToggleGroup, Tooltip. (Stock Sheet is installed but never used directly — use WindowSheet or CanvasSheet.)

Chat surfaces compose the chat vocabulary, never hand-rolled equivalents: MessageScroller owns conversation scrolling (auto-follow, yield on scroll-up, jump-to-latest — never a manual overflow container or stick-to-bottom hook); Message/Bubble own rows and surfaces; Marker owns dividers and system notes; the shimmer utility owns live-status text. Composition rules: the vendored skill at .agents/skills/shadcn/rules/chat.md. In web-v2, ChatPane owns the scroller and ChatEntryList wraps rows in MessageScrollerItem — surfaces never touch scrolling.

Layers wrappers (exist because stock defaults didn’t fit): CommandInline, DateTimePicker, HashtagTextarea, PhoneInput.

Layers components: the authoritative inventory is packages/ui/src/ (one file per component) rendered exhaustively at strata.layers.com/components — the coverage gate keeps the two identical, so a component absent from the site does not exist. Props and usage context live on each component’s page and in its source.

Variants

Variants are class-variance-authority (cva) props on the component — Button’s variant/size, Badge’s custom success variant, InputGroup and TabsList’s size (“default” 36px / “sm” 32px). Extend via variants, never a parallel component — a ComponentSmall fork drifts from the original. House variant additions are documented on each component’s site page.

Rules

  1. Never hand-roll what shadcn provides. Before building any interactive element, check the shadcn website; if a component exists, install/use it stock. A hand-rolled lookalike is a defect even if pixel-identical.
  2. No custom Tailwind on shadcn compounds — stock structure and spacing are law. Audit every className you added before reporting a surface done; the only exemptions are one-off layout constraints (a popover width) and skeleton sizing.
  3. Every SelectItem sits inside a SelectGroup (the popover padding lives on the group) — no orphan items, even for a single group.
  4. Forms never wrap gap-laid-out children. PopoverContent, DialogContent, Card, and WindowSheetContent space their DIRECT children with grid/flex gap — a <form> wrapping them becomes one child and collapses the spacing. The <form> goes inside the content region; the submit button links via form={id} from the footer. Bare <form>, zero Tailwind on it; spacing comes from FieldSet.
  5. Stock shadcn docs live on the website, nowhere else. URL pattern: https://ui.shadcn.com/docs/components/<slug> (WebFetch it). The shadcn MCP is for add install commands only — its demo registry is stale. We document only what is custom to us; if an installed component drifts from the current website docs, update the component, never document the old behavior.
  6. New component = packages/ui first. If more than one consumer could ever want it, it starts in the package, not in an app’s components/ directory. Add a catalog entry (see generative-ui.md) when Elle should be able to compose with it.

Raw markdown for agents: /docs/components.md