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 anypackages/uicomponent without a page. This doc covers the package mechanics and the cross-consumer rules.
Package mechanics
- Source-consumed.
@layers/uihas no build step — consumers transpilesrc/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-maiastyle) on@base-ui/react(Base UI). Radix is fully retired from this package: triggers take arenderprop (neverasChild), overlay anatomy is Portal > Positioner > Popup (Backdrop for scrims), and state hooks aredata-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
- 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.
- No custom Tailwind on shadcn compounds — stock structure and spacing
are law. Audit every
classNameyou added before reporting a surface done; the only exemptions are one-off layout constraints (a popover width) and skeleton sizing. - Every
SelectItemsits inside aSelectGroup(the popover padding lives on the group) — no orphan items, even for a single group. - Forms never wrap gap-laid-out children.
PopoverContent,DialogContent,Card, andWindowSheetContentspace 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 viaform={id}from the footer. Bare<form>, zero Tailwind on it; spacing comes fromFieldSet. - Stock shadcn docs live on the website, nowhere else. URL pattern:
https://ui.shadcn.com/docs/components/<slug>(WebFetch it). The shadcn MCP is foraddinstall 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. - New component =
packages/uifirst. If more than one consumer could ever want it, it starts in the package, not in an app’scomponents/directory. Add a catalog entry (seegenerative-ui.md) when Elle should be able to compose with it.