Components
@memoturn/ui is the shared component library used by the Memoturn dashboard and this docs site. It's built on shadcn/ui new-york-v4, Radix UI primitives, Tailwind CSS v4, and a brand color overlay defined in tokens.css.
The library is workspace-internal today ("private": true). There's no npm release.
Foundation
Section titled “Foundation”The 57 component primitives are sourced verbatim from shadcn/ui's new-york-v4 registry. Brand identity comes through three vectors:
--primary: brand teal (lagoon-deeplight /lagoondark) appears on every button, focus ring, accent, and active state.- Display typography: one Archivo family, with the width axis (
font-stretch: 110%) rather than a second typeface separating display from body. - Atmospheric decoration:
.atoll-bandtopographic texture appears on hero/CTA/empty surfaces; brand gradients (--gradient-lagoon,--gradient-atoll) on hero badges and the user avatar.
Structural surfaces (--background, --card, --popover, --border, --muted, --accent) are dark-first: :root carries the sea-ink-tinted instrument panel (--background: oklch(0.132 0.004 215), never pure #000) and [data-theme="light"], .light is the reading-mode override. Borders and rules are foam tints on dark, sea-ink tints on light. The docs landing (HeroCard, LinkChip, FeatureCard, CTA) uses --docs-panel-bg (sand-tinted page-white in light, card-tinted in dark) with hairline --docs-line borders; the closing-band CTA lights up the brand --gradient-atoll as the page's single brand moment.
Tokens
Section titled “Tokens”Defined in packages/ui/src/styles/tokens.css. Composed via Tailwind v4 @theme inline so every token is addressable as a Tailwind utility (bg-card, text-primary, border-input, etc.).
Brand palette (always addressable)
Section titled “Brand palette (always addressable)”| Token | Light | Dark | Use |
|---|---|---|---|
--lagoon | #4fb8b2 | #60d7cf | Bright accent (bg-lagoon) |
--lagoon-deep | #328f97 | #8de5db | Deep accent (text-lagoon-deep) |
--palm | #2f6a4a | #6ec89a | Secondary accent / success |
--sand / --foam | warm pales | dark scaled | Atmospheric tints |
--accent-warm | #e89456 | #f5b169 | Warm accent (warnings, CTA highlights) |
These exist for hero/CTA decoration. Don't reach for them in primitives. Primitives consume the semantic tokens below, which already route through --primary etc.
Semantic tokens (shadcn surface)
Section titled “Semantic tokens (shadcn surface)”| Family | Tokens |
|---|---|
| Surfaces | --background, --card, --popover, --surface (muted panels), --sidebar |
| Text | --foreground, --card-foreground, --muted-foreground, --popover-foreground |
| Action | --primary (brand teal), --secondary, --accent, --destructive, --success, --warning |
| Form | --border, --input, --ring (also brand teal) |
| Code | --code, --code-foreground, --code-highlight, --code-number |
| Chart | --chart-1 through --chart-5 (brand-sequenced) |
| Selection | --selection, --selection-foreground |
| Overlay | --overlay, --overlay-strong, --overlay-subtle (three-tier) |
Every token has a dark counterpart that flips on :root[data-theme="dark"], .dark class, or @media (prefers-color-scheme: dark).
Radius
Section titled “Radius”--radius defaults to 0.375rem (~6px). Derived scale: --radius-sm through --radius-4xl via calc(var(--radius) * N). Use Tailwind utilities: rounded-sm, rounded-md, rounded-lg, rounded-xl.
Shadow
Section titled “Shadow”Resting surfaces carry no shadow — depth comes from hairline rules, the canvas → panel tonal step, and --glow-lagoon, a static radial lagoon wash falling from a surface's top edge. shadow-md / shadow-lg remain for overlays (modal, popover, sheet, tooltip, dropdown), where elevation is the meaning.
Typography
Section titled “Typography”- Body / UI / display: Archivo (variable; weight 400–800, width 100–125),
--font-sans/--font-heading - Code: system mono stack,
--font-mono/font-mono
One variable family carries the whole surface. At normal width Archivo is body and UI; at font-stretch: 110% with -0.03em tracking and weight 700 it becomes the display voice — compact, geometric, machine-set. A second sans, a serif, or italics is a flag to revise.
Display steps
Section titled “Display steps”Four named sizes carry the display scale, each bundling size, leading, tracking and weight, so a heading needs one utility rather than an inline clamp() repeated at every call site:
| Step | Size | Use |
|---|---|---|
text-hero | clamp(2.75rem, 6.5vw, 5.25rem) | The one h1 on a marketing page. |
text-display | clamp(2rem, 4vw, 3rem) | Major section openers. |
text-section | clamp(1.875rem, 3.8vw, 2.875rem) | Ordinary section headings. |
text-band | clamp(1.875rem, 4.5vw, 3.5rem) | Headline inside a full-bleed band. |
Tracking rides the size — the larger the step, the tighter it is set, so the scale sits on one optical curve rather than sharing a flat value. -0.04em remains the floor.
Pair a step with .display-title, which carries the family and the width axis (font-stretch has no Tailwind sub-token slot, so it cannot live in the theme): class="display-title text-section text-foreground".
Two utility classes give you the brand voice:
.kicker: small uppercase tracked label (KICKER LIKE THIS). Composes with margin/color modifiers:class="kicker mb-2 text-primary"..display-title: Archivo 700 at 110% width,-0.03emtracking,1.04line-height,text-wrap: balance. Use for hero/section/card titles. Defined in@layer components, so any utility written beside it — atext-*step,tracking-tight— wins. Unlayered it beat every utility silently, which discarded overrides several call sites already asked for..tone-soft:--muted-foreground. The recessive half of a two-tone headline — the section-header system that replaced uppercase eyebrows..frame-shot/.glow-panel/.lit-edge: the capture frame, a cell that lights on hover, and a centre-anchored lagoon hairline on a section boundary..rule-gradient: a 1px frame painted as a gradient and masked so only the border survives, giving a panel a lit top lip with no shadow and no hue. Where.lit-edgelights one horizontal seam in lagoon, this frames a panel in ink. Apply it through<RuleEdge />, never directly:maskcomposites an element and its descendants, so the bare class on a panel clips that panel's own text away..panel-noise: sub-perceptualfeTurbulencegrain at 3.5%, inline as a data URI. A flat OKLCH fill this dark bands across a wide surface; the grain breaks it up and gives the surface tooth without touching text contrast.
Page-shell utilities
Section titled “Page-shell utilities”These define the dashboard's visual rhythm without touching component primitives:
.page-wrap: fluid full-width container, soft cap at 1800px, padding scalesclamp(1rem, 3vw, 3rem)..atoll-band: topographic concentric-arc texture at 6% opacity with a radial mask; compose on cards or sections for ambient depth..rise-in: entrance animation; respectsprefers-reduced-motion..shimmer: left-to-right brand-tinted gradient sweep; powers theSkeletonprimitive..nav-link: underline indicator that grows from left on hover/active; brand-teal stroke.
Primitives
Section titled “Primitives”All 57 shadcn new-york-v4 components are exported from @memoturn/ui. Categories below highlight the most-used; the full list is in packages/ui/src/index.ts.
Form controls
Section titled “Form controls”Button, Input, Textarea, Select, NativeSelect, Checkbox, RadioGroup, Switch, Slider, Label, Form (react-hook-form integration), Field, FieldGroup, InputGroup, InputOTP, Combobox, Toggle, ToggleGroup, ButtonGroup.
Every form control shares one focus language: ring-2 ring-ring on focus, hover:border-ring/40 preview, brand-teal ring (because --ring resolves to --primary). The controls that carry a solid --primary fill when active (Button, Checkbox, Switch) add ring-offset-2 ring-offset-background: a ring drawn straight onto that fill measures 1:1 against it and vanishes at exactly the moment the control is on.
Overlays
Section titled “Overlays”Dialog, AlertDialog, Sheet, Drawer, Popover, HoverCard, Tooltip, DropdownMenu, ContextMenu, Menubar, Command (CommandDialog for ⌘K palettes).
All use bg-popover for content and the brand-tinted overlay scrim. data-slot attributes on every element for inspect-and-style.
Layout
Section titled “Layout”Card (+ CardHeader/CardTitle/CardDescription/CardContent/CardFooter/CardAction), Sidebar, Resizable panels, ScrollArea, Separator, AspectRatio, Tabs.
CardTitle / DialogTitle / SheetTitle / DrawerTitle / EmptyTitle use font-heading (Archivo). Every modal display moment is in the brand voice.
Data display
Section titled “Data display”Table, Badge, Avatar (+ AvatarFallback / AvatarBadge / AvatarGroup), Item (data row primitive), Empty (EmptyTitle/EmptyDescription/EmptyMedia), Pagination, Breadcrumb, Accordion, Collapsible, Calendar, Carousel, Chart (recharts wrapper), Progress, Skeleton, Spinner, Kbd.
Badge extends shadcn defaults with three brand variants (dense primary tint, lexical palm tint, hot warm tint) used for search-leg color coding. Skeleton uses .shimmer for brand-tinted loading.
Toasts
Section titled “Toasts”Toaster (Sonner-backed) + the toast helper. Mount once at the app root:
import { Toaster } from "@memoturn/ui";
export function RootLayout() { return ( <> {/* …app… */} <Toaster /> </> );}Then fire from anywhere:
import { toast } from "@memoturn/ui";
await revokeKey(id);toast.success(`Revoked "${name}"`);
// or with a descriptiontoast.error({ title: "Revoke failed", description: err.message });The helper exposes success, error, info, and dismiss. Each type gets a left-border color stripe (success → palm, info → primary, warning → accent-warm, error → destructive) and a brand-tinted icon.
Composites
Section titled “Composites”Higher-level building blocks specific to the Memoturn brand voice. They compose primitives plus brand decoration (atoll-band, gradients, kicker labels).
| Component | Source | Use |
|---|---|---|
Hero | Hero.tsx | Landing / section hero with kicker, title, description, action slots. |
FeatureCard | FeatureCard.tsx | Icon + kicker + title + description + footer; lifts on hover. |
CTA | CTA.tsx | Brand-gradient call-to-action panel with primary + secondary actions. |
EmptyState | EmptyState.tsx | Centered empty state with BrandMark, title, description, action. |
CodeBlock | CodeBlock.tsx | Snippet block with copy-to-clipboard button. |
Logo | Logo.tsx | 4 variants: full, mark, wordmark, chip. |
BrandMark | BrandMark.tsx | Standalone SVG mark. |
ThemeToggle | ThemeToggle.tsx | Light / dark / auto switcher; persists to localStorage. |
RuleEdge | RuleEdge.tsx | The decorative overlay that carries .rule-gradient. Parent needs relative. |
SignalField | SignalField.tsx | The hero's live texture: a 96px ruled grid in CSS with turns travelling it on a canvas above. Suspends off-screen, below 640px, and under prefers-reduced-motion, which paints the grid alone rather than a blank surface. |
Adding a shadcn component
Section titled “Adding a shadcn component”The dashboard wires apps/app/components.json so the shadcn CLI installs into packages/ui/src/components/:
pnpm exec shadcn add <component>After install, normalize the imports to relative paths (@/lib/utils → ../lib/utils, @/components/ui/... → ./...) and re-export from packages/ui/src/index.ts.
Theming consumers
Section titled “Theming consumers”Both the Memoturn dashboard (TanStack Start on Workers) and this docs site (Astro + Starlight) consume tokens.css directly. The dashboard mounts via Tailwind v4's @theme inline directive; the docs site @imports and adds back-compat aliases for older brand-tinted decoration.
To embed @memoturn/ui in a third app, import the tokens once at the top of your global stylesheet, then add the Tailwind layer:
@import "tailwindcss";@import "tw-animate-css";@import "@memoturn/ui/styles/tokens.css";@import "@memoturn/ui/styles/base.css";
@source "../node_modules/@memoturn/ui/src/components/**/*.{ts,tsx}";
@custom-variant dark (&:is([data-theme="dark"] *, .dark *));Accessibility
Section titled “Accessibility”- Every interactive primitive has a visible
focus-visiblering driven by--ring(brand teal). - Radix-backed components inherit Radix's keyboard and screen-reader support.
- Form inputs respect
aria-invalid(destructive ring + border) and pair with<Label htmlFor>. Emptyand error boundaries use semantic headings; modal titles use properDialog.Title.- Reduced motion:
.rise-inand.shimmerhonorprefers-reduced-motion: reduce.
If you find a component that fails an axe-core check, please open an issue on GitHub. Accessibility regressions are bugs, not enhancements.