# File: SKILL.md --- name: vireya description: Use when building or modifying React UIs with Vireya (`@vireya/core`, `@vireya/ui`, `@vireya/next`, `@vireya/blocks`). Triggers on "vireya", "@vireya/*", or asks to build a landing/marketing/dashboard page with these packages. --- # Vireya Token-first React design system on Next.js. You consume `@vireya/*` from npm — no source access needed. The DTS at `node_modules/@vireya/{ui,blocks}/dist/components///index.d.ts` is the source of truth for props. ## How to use this skill This file is a **dispatcher**, not a manual. It carries the rules + a token cheat sheet + a routing table to references. Don't read every reference upfront — load only the one you need for the current task. - For a component's props → open the DTS, not a reference. - For setup, gotchas, or a copy-paste recipe → open the matching `references/.md`. - For comprehensive offline usage (no network) → `node_modules/@vireya/ui/LLMS.md` is bundled with the package. ## Reach for what exists — do NOT rebuild **Before you build any interactive pattern from lower-level primitives, assume Vireya already ships it.** Vireya is a *batteries-included* system: there's a component or block for almost every common UI need. Hand-rolling an accordion out of `Collapsible`, a tab bar out of buttons, or a FAQ out of raw `
` is a **bug**, not a shortcut — you lose the tokens, a11y, animation, and theme behavior that the real component already solved. When an intent matches the table, use the named import. Don't reason your way to a primitive. | You're building… | Use (don't rebuild) | |---|---| | FAQ section | `@vireya/blocks/faq/accordion` or `@vireya/blocks/faq/twoColumn` | | Expand/collapse Q&A, "show more" list, settings sections | `@vireya/ui/typography/accordion` (multi-item, indicators, surfaces). A **single, lone** toggle only → `@vireya/ui/data/collapsible` | | Tabbed content | `@vireya/ui/data/tab` | | Modal / confirm dialog / side panel / anchored popup / hover hint / action menu | overlay picker → `@vireya/ui/overlay/{dialog,alertDialog,sheets,drawer,popover,tooltip,dropdownMenu}` (which-to-pick rules in `references/design.md`) | | Hero · pricing · CTA · testimonials · footer · navbar · logo cloud · contact | the matching `@vireya/blocks/*` family (10 families, 26 blocks) | | Stepper · checklist · feature grid · alternating feature rows | `@vireya/blocks/feature/*` | | Data grid · date picker · OTP input · color picker · file upload · command palette · breadcrumb · sidebar · tree view | `@vireya/ui/data/dataTable`, `@vireya/ui/form/*`, `@vireya/ui/data/command`, `@vireya/ui/navigation/*`, `@vireya/ui/layout/*` | **If you're unsure whether something exists, list it — don't assume it doesn't:** ```sh ls node_modules/@vireya/ui/dist/components/*/ # every UI group/component ls node_modules/@vireya/blocks/dist/components/*/ # every block family ``` The subpath is always `@vireya/ui//` (or `@vireya/blocks//`). Open the component's DTS for props. **The only legitimate escape hatch when a component *almost* fits is `className` / `style` / `asChild`** — never a from-scratch reimplementation. Deep taxonomy ("never re-implement a block as primitives if the block exists") in `references/design.md`. ## Non-negotiable rules 1. **Tokens are absolute.** Every CSS visual property → `var(--v-*)`. No hardcoded `px`, `#hex`, `rgb()`, `rgba()`, numeric `font-weight`, or font-family literals. Allowed: `0`, `100%`, `1px`/`1.4px` for inline-SVG hairlines, `currentColor` in icons, `%` inside `calc()`. Pill/circle uses `--v-radius-full`. If a token is missing for your case, override locally via inline `style` — don't invent literals. 2. **`primary` ≠ `accent`.** primary = neutral-strong (text, default CTAs, default borders). accent = brand-vivid (promo CTAs, "Popular" badge, focus rings, eyebrow dots), **optional**, falls back to primary. Always chain accent fallbacks in CSS: ```css background-color: var(--v-accent-fill, var(--v-primary-fill)); color: var(--v-accent-foreground, var(--v-primary-foreground)); border-color: var(--v-accent-border, var(--v-primary-border)); ``` Components exposing `variant="accent"`: **`Button` and `Badge` only**. 3. **Pin `react@19.2.1` exactly** (and `react-dom`). Vireya peers React 19 on every package. Mismatches cause duplicate-React runtime errors — `pnpm dedupe` if your tree hoists multiple. 4. **Per-component subpath imports only.** `import { Button } from "@vireya/ui/form/button"`. The **only** allowed root import is `import { AppProvider } from "@vireya/ui"`. Locales are default exports: `import enUS from "@vireya/ui/locales/enUS"`. Same for blocks: `@vireya/blocks/hero/centered`. 5. **Focus + disabled.** `:focus-visible { outline: 2px solid var(--v-primary-fill); outline-offset: 2px; }` — never bare `:focus`, never `outline: none` without a replacement. Disabled = `opacity: 0.5; cursor: not-allowed;` — universal, no custom gray. 6. **Reach for the existing component first.** See "Reach for what exists" above. Rebuilding an Accordion / Tabs / FAQ / Modal / Drawer that already ships is a bug — enumerate the inventory before composing primitives. Being inside a server (RSC) page is **not** a reason to avoid a component or rebuild it (see gotcha #4). ## Token cheat sheet Full catalog in `references/tokens.md`. The table is the *what*; the two decision blocks below are the *which* — read them, colors and sizes are where agents err most. | Property | Token | |---|---| | `font-size` | `var(--v-font-size-{caption,body-sm,body,body-lg,title-sm,title,title-lg,display,display-lg})` or `` | | `font-weight` | `var(--v-font-weight-{regular,medium,semibold,bold})` or `` | | `font-family` | `var(--v-font-family-{sans,mono})` (mono for code/CLI/path/URL only) | | `line-height` | `var(--v-leading-{tight,snug,normal,relaxed})` | | `letter-spacing` | `var(--v-tracking-{tight,normal,wide,eyebrow})` | | `padding`, `margin`, `gap`, offsets | `var(--v-{height,width}-N)` (N = 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 32, 40, 48, 64, 80, 96, 128) | | Outer page gutter | `var(--v-page-padding)` | | `border-radius` | `var(--v-radius-{xs,sm,md,lg,xl,2xl,full})` | | Colors | `var(--v-{name}-{fill,foreground,hover,active,subdued,border})` — **never pick sub-tokens à la carte; choose a surface row in "Colors" below** | | `transition-duration` | `var(--v-motion-{instant,fast,default,slow,slower})` | | `transition-timing-function` | `var(--v-motion-timing)` | | `box-shadow` | `var(--v-elevation-{rest,hover,popover,modal})` | | Inner content max-width | `var(--v-content-width-{sm,md,lg})` | | Field/avatar/badge/icon sizing | Composites: `var(--v-{field,avatar,badge,icon}-{ss,sm,md,lg,xl}-*)` | ### Colors — decide by surface, never sub-token by sub-token `-foreground` does **not** mean "the text color". It means **the text/icon color that sits ON THIS TIER'S `-fill`**. So you only reach for `--v-X-foreground` when the element's *own* background is `--v-X-fill`. For everything else, ask one question: **"what surface is this text on?"** → use that surface's foreground. Paint a whole surface from one row (covers ~95% of cases): | Surface | background | default text | muted / supporting text | border | |---|---|---|---|---| | Page (root, hero) | `--v-background-fill` | `--v-background-foreground` | `--v-secondary-active` | — | | Card / nested container / sticky strip | `--v-secondary-fill` | `--v-background-foreground` | `--v-secondary-active` | `--v-secondary-border` | | Solid CTA (default) | `--v-primary-fill` | `--v-primary-foreground` | — | `--v-primary-border` | | Accent CTA / promo | `var(--v-accent-fill, var(--v-primary-fill))` | `var(--v-accent-foreground, var(--v-primary-foreground))` | — | `var(--v-accent-border, var(--v-primary-border))` | | Destructive | `--v-destructive-fill` | `--v-destructive-foreground` | — | `--v-destructive-border` | Three things that trip up every fresh agent: 1. **Muted text = `--v-secondary-active`**, never `--v-secondary-foreground` (that's what `` resolves to). 2. **A card tinted with `--v-secondary-fill` keeps `--v-background-foreground` for its text** — a tone shift, not a tier shift. Don't switch to `--v-secondary-foreground`. 3. **`primary`/`accent` are emphasis tiers, not body text.** They only *look* right because they're near-black/white — semantically wrong and they invert in dark mode. Page text is always `--v-background-foreground`. Pick the six sub-tokens of a tier as a unit (`fill`+`foreground`+`border`, then `hover`/`active`/`subdued` for states) — don't mix `primary-fill` with `secondary-border`. Translucency → `color-mix(in srgb, var(--v-X-fill) N%, transparent)`, never `rgba()`. Full matrix + hierarchy in `references/design.md`. ### Sizes — semantic names, snap to the scale, never a px literal **Type** — pick the *role*, not a pixel (this is the most-guessed-wrong call): | Role | size | weight | |---|---|---| | Hero | `display-lg` (64) / `display` (48) | `semibold` | | Section heading (H2) | `title-lg` (32) ← default | `semibold` | | Subsection / H3 | `title` (24) / `title-sm` (20) | `semibold` / `medium` | | Card title | `title-sm` (20) / `body-lg` (18) | `medium` | | Body | `body` (16); lead `body-lg` (18); small `body-sm` (14); caption `12` | `regular` | Weight has exactly **3 working levels**: `regular` · `medium` · `semibold` (`bold` is art-direction only). Hierarchy = size first, then weight, then color. **Spacing & sizing** — `--v-{height,width}-N`, where N is **only** from `{1,2,4,6,8,10,12,14,16,18,20,22,24,32,40,48,64,80,96,128}`. Need 30? Snap to `32`. Never an off-scale number, never a px literal. - icon↔text gap `6`–`8` · label↔input `6` · card padding `12`/`20` · header→content `48` · section padding `96`/`128` · outer gutter `--v-page-padding` **Component sizing** (button/input/avatar/badge/icon heights, paddings, fonts) → use the composites `var(--v-{field,avatar,badge,icon}-{ss,sm,md,lg,xl}-*)`; don't recompose from the raw scale. **Radius**: children smaller than parent (button `lg` inside card `xl`). ### Proportion — derive it, never measure-and-guess Proportion bugs (mismatched widths, uneven columns, controls of subtly different heights) come from *eyeballing pixels*, not from a missing feature. The fixes are deterministic: - **Siblings that should align share the SAME token.** Two cards/columns that look mismatched should both resolve to the same `--v-width-N`, or both be `flex: 1` / a grid `1fr` track — never one hand-tuned px value against another. Equal-weight items → equal tracks; let the layout, not magic numbers, equalize them. - **A whole row of controls shares one `size` prop.** Inputs, buttons, and selects in the same group all take e.g. `size="md"`; their heights/paddings/fonts then come from the same composite tokens and line up automatically. Don't size one control by hand. - **Don't start a dev server to "measure the width mismatch."** A pixel mismatch is almost always (a) an off-scale literal, (b) two siblings on different tokens, or (c) a `box-sizing`/padding asymmetry — fix by snapping both sides to one token, not by sampling rendered pixels. - **Type proportion is hierarchy, not size-tweaking:** size > weight > color. Adjacent text uses the canonical ramp; never invent an in-between size to "balance" two elements. Deeper spacing rhythm & sizing rationale in `references/design.md`. ### Top mistakes — WRONG → RIGHT Colors: - `color: var(--v-primary-fill)` / `var(--v-secondary-foreground)` for page text → invisible "X-on-X" in one theme. **→ `var(--v-background-foreground)`**. - Muted text `var(--v-secondary-foreground)` → no contrast. **→ `var(--v-secondary-active)`**. - `color: #6b7280` / `background: rgba(0,0,0,.05)` → **→ a tier token / `color-mix(in srgb, var(--v-X-fill) N%, transparent)`**. - `var(--v-accent-fill)` with no fallback → vanishes in themes without accent. **→ `var(--v-accent-fill, var(--v-primary-fill))`**. Sizes: - `font-size: 32px` (or the removed numeric `` escape) → **→ `var(--v-font-size-title-lg)` / ``**. - `padding: 30px` / `gap: 15px` (off-scale) → **→ snap to nearest token (`-32`, `-16`)**. - `font-weight: 600` / `line-height: 1.5` literals → **→ `var(--v-font-weight-semibold)` / `var(--v-leading-relaxed)`**. ## Packages | Package | Purpose | |---|---| | `@vireya/core` | Tokens, `createTheme`, color utilities | | `@vireya/ui` | React component primitives (8 groups: `form`, `data`, `feedback`, `overlay`, `navigation`, `layout`, `typography`, `common`) | | `@vireya/next` | `ThemeProvider` (server), `useTheme` (`/client` subpath), motion preset CSS files (`/motion/*`), bundled tokens (`/styles`) | | `@vireya/blocks` | Pre-composed landing-page sections (10 families, 26 blocks) | Install: `pnpm add @vireya/core @vireya/ui @vireya/next @vireya/blocks react@19.2.1 react-dom@19.2.1 next lucide-react`. Optional peers (install only when used): `@tanstack/react-table`, `recharts`, `embla-carousel-react`, `react-day-picker` + `date-fns`, `cmdk`, `vaul`, `react-resizable-panels`, `@headless-tree/core`, `react-hook-form`. Full setup recipe in `references/setup.md` or `node_modules/@vireya/ui/LLMS.md`. ## Routing table — when to read what | Need | Open | |---|---| | Setup, providers, themes, motion presets | `references/setup.md` | | Section primitive, hero CTA pair, pricing, install snippet, scoped accent recipes | `references/recipes.md` | | Color contrast pairing matrix, CTA decision matrix, emphasis rule, type roles, visual depth (gradients/spotlights/masks), anti-patterns gallery | `references/design.md` | | Block sub-component shapes (`PricingTiers.Tier`, `ContactSplit.Form` types), which blocks expose `emphasis` | `references/blocks.md` | | Full token catalog, composite tokens, every scale | `references/tokens.md` | | Type errors, RSC traps, hydration issues, animation flickers, missing icons | `references/gotchas.md` | | **Upgrading from a pre-0.1.0 `@vireya/*` (token API reshape, codemod, manual fixes)** | `references/migration.md` | | Detailed props of a specific component | DTS at `node_modules/@vireya/ui/dist/components///index.d.ts` | | Comprehensive offline reference | `node_modules/@vireya/ui/LLMS.md` (bundled in tarball) | ## Top 5 gotchas Full list in `references/gotchas.md`. Failures that cost most time: 1. **`AppProvider` is mandatory** with the full 14-icon map + locale. Missing icons silently break `DateField`, `Calendar`, `Select` chevron, `Toast` close, `DropdownMenu` checks. See `references/setup.md`. 2. **`accent` needs fallback chain in every CSS reference.** Themes without `accent` leave those vars `undefined` → accent CTAs vanish in dark mode. 3. **No barrel imports.** `@vireya/ui` root only exports `AppProvider`. Subpath everything else. 4. **`"use client"` only blocks *non-component named exports* across the RSC boundary — not the components themselves.** Rendering ``, any `@vireya/ui` component, or any `@vireya/blocks` block *inside a server page is correct and expected* — Next.js renders client components in server trees fine. The only trap: importing a **non-component named export** (a `meta` const, a config object) from a `"use client"` module into a server module returns `undefined` — bake those at build time or move them to a server-only file. **This is never a reason to avoid, wrap, or rebuild a component.** 5. **``** is required — the theme bootstrap script runs before React hydrates. ## Self-audit before declaring a page done - [ ] No hardcoded `px`/`#hex`/`rgba()`/numeric `font-weight` in your CSS Modules (allowed: `0`, `100%`, `1px`/`1.4px` SVG hairlines); spacing values are on-scale (snap off-scale to nearest). - [ ] Text color comes from its surface's foreground — page/card text is `--v-background-foreground`, muted is `--v-secondary-active`; no `*-foreground` used away from its own `*-fill`, no `primary`/`accent` as body text. - [ ] Every `--v-accent-*` reference is chained to a `--v-primary-*` fallback. - [ ] `:focus-visible` defined on every interactive element (no bare `:focus`, no `outline: none` without replacement). - [ ] Every `@vireya/ui` and `@vireya/blocks` import is a subpath (only `AppProvider` may come from the root). - [ ] `` set in `app/layout.tsx`; `AppProvider` wired with the full icon map + locale. - [ ] React `19.2.1` pinned in `package.json`. - [ ] Dark theme survives a toggle: no invisible text, no disappearing accent CTAs. - [ ] Mobile (640px wide): section padding shrinks, no horizontal scroll. - [ ] No twin `solid` CTAs of the same variant in one group — at most one solid; siblings demote to `outlined`/`ghosted`. - [ ] At most one accent moment per viewport-fold. --- # File: references/design.md # Vireya design guidelines — making decisions, not just following rules This is the **decision layer**: when you face a fork (which color, which component, which spacing step), pick the right one. Token names and scales live in `tokens.md`. This file is about **judgment**. --- ## Foundational principles These are non-negotiable. Every decision below derives from one of them. 1. **Token-first — ABSOLUTE.** Visual properties go through `var(--v-*)`. If the right token doesn't exist, override locally via inline `style` or a CSS Module variable, then open an issue at [`vireya-ui/skills/issues`](https://github.com/vireya-ui/skills/issues) so it can be added upstream. Don't proliferate one-off literals — concentrate them in one CSS module if multiple uses appear. 2. **Composition over configuration.** Vireya components avoid prop explosion: when variation grows, they expose slots (`ReactNode` props) or `asChild`. Apply the same rule to your own wrappers. 3. **Server-renderable by default.** Most Vireya components are server-renderable. Reach for `"use client"` only in your own components when you genuinely need state, refs, or browser APIs. 4. **Escape hatches always available.** Vireya never traps you: `className`, `style`, `asChild`, `Styles` reexport. Use them before forking. 5. **Accessibility is a baseline, not a feature.** `:focus-visible`, ARIA, keyboard, reduced-motion, contrast — floors, not opt-ins. Vireya enforces these internally; mirror the same in your own components. --- ## Color decisions ### Contrast pairing matrix — pick a row, paint the surface The single biggest source of "ugly / no-contrast" Vireya UIs is mis-pairing color sub-tokens. Internalize this table — it covers 95% of cases: | Surface tier | Background | Default text | Muted / supporting text | Border | |---|---|---|---|---| | Page (root, hero) | `--v-background-fill` | `--v-background-foreground` | `--v-secondary-active` | — | | Card / nested container / sticky header strip | `--v-secondary-fill` | `--v-background-foreground` | `--v-secondary-active` | `--v-secondary-border` | | Solid CTA (default) | `--v-primary-fill` | `--v-primary-foreground` | — | `--v-primary-border` | | Accent CTA / promo surface | `var(--v-accent-fill, var(--v-primary-fill))` | `var(--v-accent-foreground, var(--v-primary-foreground))` | — | `var(--v-accent-border, var(--v-primary-border))` | | Destructive | `--v-destructive-fill` | `--v-destructive-foreground` | — | `--v-destructive-border` | **Two surprises worth memorizing:** 1. **Muted text uses `--v-secondary-active`, NOT `--v-secondary-foreground`.** `*-foreground` is reserved for "text painted ON the matching `*-fill`". For low-emphasis text on a normal page background, `--v-secondary-active` is what `` resolves to. 2. **A nested card with `secondary-fill` background still uses `background-foreground` for default text** — not `secondary-foreground`. The text color is determined by the *page* foreground, not the local fill, because the nested container is a tone shift, not a tier shift. ### The primary / accent split `primary` and `accent` are NOT interchangeable. They serve different roles: | Color | Identity | Use for | |---|---|---| | `primary` | neutral-strong (near-black on light, near-white on dark) | body text, default CTAs, icons, default borders, neutral foreground | | `accent` | brand-vivid (magenta, teal, green, etc.) | promotional CTAs, "Popular" badge, focus rings, eyebrow dots, illustrative highlights | This is the same split used by Vercel, Linear, and Stripe Press. **It exists so a consumer can ship a magenta `accent` without making body text magenta.** **Rules:** - Components that expose `variant="accent"`: **`Button` and `Badge` only**. Other components consume accent implicitly via roles (focus rings, selection states). - `accent` is **optional**. Themes that don't declare it must render visually identical to a pre-accent design. - **Always chain the fallback** in CSS: `var(--v-accent-fill, var(--v-primary-fill))`, same for every sub-token (`-foreground`, `-hover`, `-active`, `-subdued`, `-border`). - **Never put two `primary` Buttons in the same form.** Promote one (the actual decision) and demote the other to `secondary`/`outlined`/`ghosted`. Two equally-loud CTAs = no CTA. ### Color hierarchy on a surface Reach for these in order. If you find yourself reaching for #4 or #5, double-check the layout first. 1. `var(--v-background-fill)` — page bg, default surface 2. `var(--v-secondary-fill)` — subtle surfaces, muted fills, disabled bg 3. `var(--v-primary-fill)` — strong CTAs, foreground text on background 4. `var(--v-accent-fill, var(--v-primary-fill))` — branded moments only (one per surface, max) 5. `var(--v-destructive-fill)` — errors, destructive actions only 6. `var(--v-link-fill)` — inline hyperlinks only ### Sub-token pairing — pick all six together Every named color has 6 sub-tokens. You don't pick which to use — you pick the **role** and pair them correctly: ```css .solid { background-color: var(--v-primary-fill); color: var(--v-primary-foreground); border: 1px solid var(--v-primary-border); } .solid:not(:disabled):hover { background-color: var(--v-primary-hover); } .solid:not(:disabled):active { background-color: var(--v-primary-active); } .solid[aria-disabled="true"] { background-color: var(--v-primary-subdued); } ``` **Anti-pattern — sub-token mix-and-match:** ```css /* WRONG — pairing primary fill with secondary border by hand */ background: var(--v-primary-fill); border: 1px solid var(--v-secondary-border); /* should be --v-primary-border */ ``` If the auto-derived border looks wrong for a specific theme, **override it in `createTheme({ primary: { fill, border: "..." } })`** — don't hardcode in the component. ### Surface tier conflict — pick one A surface gets its tone from EITHER its container OR a `variant="shaded"` prop, never both: - ❌ `` inside a section already using `var(--v-secondary-fill)` as bg → two stacked shadeds collapse the visual rhythm - ✅ Pick the outer surface tier. Inner cards stay `variant="outlined"` (border only). ### Color forbidden moves - ❌ Don't introduce a 4th base color (success, warning, info) ad-hoc in a component. Add it to the theme contract via `createTheme` or compose `destructive` + neutral. - ❌ Raw `rgba()` for transparency. Use `color-mix(in srgb, var(--v-{name}-fill) N%, transparent)` (already a pattern in Button shaded surface). - ❌ Hardcoded hex/gray. `#e5e5e5` is `var(--v-secondary-border)`. `#f5f5f5` is `var(--v-secondary-fill)`. Always. - ❌ Custom disabled gray. Disabled = `opacity: 0.5; cursor: not-allowed;` — universal. --- ## Typography decisions ### Hierarchy is built with size > weight > color, in that order Texto pequeno em peso alto fica grosso e compete com headings. **Size is the primary lever**; weight is secondary. A `card title 18px @ medium` reads stronger than `18px @ semibold` when the H2 above is also semibold (contraste preservado). ### Three weight levels — that's the contract | Weight | When to use | |---|---| | `regular` (400) | Body text, paragraphs, descriptions, table cells | | `medium` (500) | UI assertion: eyebrows, badges, buttons, tabs, nav links, card titles ≤20px, form labels, table headers, chart labels | | `semibold` (600) | Visual dominance: hero display, section headings, large H3 (≥24px), pricing prices, brand logo | | `bold` (700) | **Reserved.** Art-direction only — explicit `weight="bold"` on a single element | Never use `300` (light) or `800/900` (black). Inconsistent across fonts and breaks the 3-level hierarchy. ### Canonical role → token mapping for sections When building a block or page, lean on this table instead of guessing: | Role | size | weight | leading | tracking | |---|---|---|---|---| | Hero display | `display-lg` (64) | `semibold` | `tight` | `tight` | | Hero medium | `display` (48) | `semibold` | `tight` | `tight` | | H2 (section) | `title-lg` (32) | `semibold` | `snug` | `tight` | | H3 large (alternating feature) | `title` (24) | `semibold` | `snug` | `normal` | | H3 card / tier | `body-lg`–`title-sm` (18–20) | `medium` | `snug` | `normal` | | Pricing price (display) | `display` (48) | `semibold` | `tight` | `tight` | | Body lead | `body-lg` (18) | `regular` | `relaxed` | `normal` | | Body default / section description | `body` (16) | `regular` | `relaxed` | `normal` | | Body small | `body-sm` (14) | `regular` | `relaxed` | `normal` | | Eyebrow | `caption` (12) | `medium` | `normal` | `eyebrow` + `text-transform: uppercase` | | Button / Badge / Tab | (field-font) | `medium` | `normal` | `normal` | **Section H2 calibrated default is `title-lg` (32px), not 36.** Marketing pages use `` paired with `` description. Mobile (`@media (max-width: 640px)`) shrinks the title to `var(--v-font-size-title) !important`. Use `display`/`display-lg` only for hero/art-direction headings (rare). ### Typography forbidden moves - ❌ `

` — use ``. - ❌ Inventing `` or any name outside the 9-step ramp. Scale: `caption · body-sm · body · body-lg · title-sm · title · title-lg · display · display-lg`. - ❌ `line-height: 1.55;` literal in CSS Modules. Always `var(--v-leading-relaxed)` (or `tight`/`snug`/`normal`). - ❌ `letter-spacing: -0.02em;` literal. Always `var(--v-tracking-tight)` (or `normal`/`wide`/`eyebrow`). - ❌ `font-weight: 500;` in CSS Modules. Always `var(--v-font-weight-medium)`. - ❌ `` numeric. Numeric escape hatch was removed in 0.1.0 — pick the semantic name. --- ## Spacing rhythm ### The ladder Think in steps. Each "level" of grouping uses a value 1–2 steps higher than the previous: | Step | Token | Use | |---|---|---| | Inline | `--v-{height,width}-2` to `-8` | Internal component spacing, icon gap, label-to-input | | Tight | `--v-height-12` | eyebrow → title | | Standard inline | `--v-height-16` | row gaps, default gap inside a card | | Group | `--v-height-24` | badge → title group, tier feature list | | Major group | `--v-height-32` | title group → actions | | Section internal | `--v-height-48` to `-64` | header → grid, intro → content | | Section padding compact | `--v-height-96` | logoCloud, footer, mobile sections | | Section padding standard | `--v-height-128` | hero, feature, pricing | **Rule:** never mix two adjacent steps in the same axis without intent. If a value you need isn't in the scale (e.g. 30px), **pick the nearest token (28 or 32)** — almost certainly the design wants the rhythm, not the literal. ### Section vertical rhythm — the canonical block template Every section block follows this pattern. Mirror it when composing custom sections: ``` section padding-top : --v-height-128 (hero/feature) or --v-height-96 (compact) header → content gap : --v-height-48 to --v-height-64 title → subtitle gap : --v-height-12 title group → actions : --v-height-32 section padding-bottom : same as top ``` Mobile (`@media (max-width: 768px)`): drop padding to `--v-height-96`, shrink display titles to ~32–40px via `font-size: Npx !important` (only legitimate `!important` use — overrides `Text`'s inline `--font-size`). ### `--v-page-padding` is the outer gutter Use `--v-page-padding` for the outermost horizontal padding of any section. Never substitute a numeric token (`--v-width-16`) for the page gutter — apps may override the page gutter to a different rhythm. ### Proportion — equalize with tokens and layout, not with measured pixels Mismatched widths, uneven columns, and controls of subtly different heights are the most common "it looks off" bugs. They are **deterministic to fix** — never reach for a dev server to measure the gap and hand-tune a px value. - **Equal-weight siblings get equal tracks.** Columns/cards meant to align should be a CSS grid (`grid-template-columns: repeat(N, 1fr)`) or `flex: 1` items — the layout equalizes them at any viewport. If they must be a fixed size, both pull the **same** `--v-width-N`. Two different hand-picked widths is the bug. - **One `size` prop per control row.** Inputs, buttons, selects, and badges in the same group all take the same `size` (`sm`/`md`/`lg`). Their heights, paddings, and fonts then derive from the same `--v-{field,…}-{size}-*` composites and align automatically. Never size one control with raw tokens while its neighbor uses a `size` prop. - **A width mismatch is a cause, not a measurement.** It's almost always (a) an off-scale literal, (b) two siblings on different tokens, or (c) asymmetric padding / `box-sizing`. Diagnose the cause and snap both sides to one token — don't sample rendered frames and nudge. - **Proportional whitespace follows the ladder.** Gaps between paired elements scale with their grouping level (above), not with the elements' own size. Don't widen a gap "to balance" a larger heading — bump it one ladder step or leave it. - **Aspect ratio for media, not magic heights.** Image/illustration/video wells use `aspectRatio` (the `@vireya/ui/layout/aspectRatio` component or the CSS prop with a clean ratio like `16/9`), so they scale with their column instead of fighting it with a fixed px height. --- ## Radius hierarchy — children smaller than parent Rounded shapes should **increase with element size**, and **inner elements take a smaller radius than their container**: | Element | Token | |---|---| | Hairline (chart inner element) | `--v-radius-xs` | | Sharp (chart bar, micro indicator) | `--v-radius-xs` | | Inline (badge, chip, tag, tooltip) | `--v-radius-sm` | | Small (input, alert, dropdown menu) | `--v-radius-md` | | Medium (button `shape_square`) | `--v-radius-lg` | | Large (card, feature item, dialog) | `--v-radius-xl` | | X-large (pricing tier, hero card, drawer) | `--v-radius-2xl` | | Pill / circle | `--v-radius-full` (= 9999px) or `50%` for circle | **Rule:** children take a smaller radius than the parent. A button (`--v-radius-lg`) inside a card (`--v-radius-xl`) — never the inverse. Step down the scale: `2xl → xl → lg → md → sm → xs`. **Anti-patterns:** - ❌ `--v-radius-sm` on a 200px-tall card (looks sharp/cheap) - ❌ `--v-radius-2xl` on a 24px badge (looks like a balloon) - ❌ `9999px` on anything that isn't a true pill (avatar, status dot, single-line tag) --- ## Elevation — Vireya is low-elevation We prefer **borders + subtle background tint over shadows**. Shadows appear only on: 1. Hover lifts (cards, tier cards) — soft, short-throw 2. Floating layers (popover, dropdown, tooltip, modal) — defined, more spread | Layer | box-shadow | |---|---| | Resting (card at rest) | **none** — use `border: 1px solid var(--v-secondary-border)` instead | | Hover lift | `0 8px 24px -12px rgba(0, 0, 0, 0.08)` | | Pricing/featured hover | `0 12px 32px -16px rgba(0, 0, 0, 0.10)` | | Popover / dropdown | `0 8px 24px -8px rgba(0, 0, 0, 0.12)` | | Modal / drawer | `0 24px 48px -16px rgba(0, 0, 0, 0.20)` | **Forbidden:** - ❌ Multi-layer shadows (`box-shadow: 0 1px 2px ..., 0 4px 8px ...`) - ❌ Warm tints (`rgba(20, 10, 0, 0.1)`) — shadows are pure black + low alpha - ❌ Resting shadow on non-floating surfaces — depth comes from border + tone (These are inline today; tokenizing as `--v-elevation-{rest,hover,popover,modal}` is a tracked gap.) --- ## Motion — animate state changes, not vibes ### Budget | Role | Duration | |---|---| | Instant | `0ms` — focus ring (immediate, communication) | | Fast | `150ms` — micro-interactions (hover color, focus ring fade) | | Default | `var(--v-motion-duration)` (250ms) — state transitions, accordion expand | | Slow | `400ms` — drawer slide, page-level reveal | | Slower | `600ms` — orchestrated, sequenced reveals | ### Easing Default `var(--v-motion-timing)` (`cubic-bezier(0.4, 0, 0.2, 1)`, material standard) covers ~95% of cases. When you need: - **Enter** (decelerating in): `cubic-bezier(0, 0, 0.2, 1)` (ease-out) - **Exit** (accelerating out): `cubic-bezier(0.4, 0, 1, 1)` (ease-in) - **Emphasized/playful**: `cubic-bezier(0.68, -0.55, 0.27, 1.55)` (overshoot) **Linear easing is wrong for everything except progress bars and loading shimmer.** ### What to animate, what NOT to - ✅ State changes that change layout (open/close, expand/collapse) - ✅ Hover color shifts (≤ default duration) - ✅ Focus ring fade-in - ❌ Hover transform lifts on touch (use `@media (hover: hover) and (pointer: fine)` to gate) - ❌ `transition: all` (always name properties) - ❌ Mandatory entrance animations with no reduced-motion fallback ### Reduced motion is mandatory Always wrap meaningful animation: ```css @media (prefers-reduced-motion: reduce) { .myAnim { animation: none; transition: none; } } ``` Hover color shifts and focus rings are **exempt** — they're communication, not motion. --- ## States — every interactive component must distinguish all of these | State | Trigger | Visual | |---|---|---| | Default | At rest | Base color, base border | | Hover | `:hover` (pointer only — gate with `@media (hover: hover)` for transforms) | `var(--v-{name}-hover)` bg or border lift | | Active / Pressed | `:active` | `var(--v-{name}-active)` or compress (transform ≤ 1px) | | Focus-visible | `:focus-visible` (NEVER `:focus` alone) | `outline: 2px solid var(--v-primary-fill); outline-offset: 2px;` | | Disabled | `:disabled` / `[aria-disabled="true"]` | `opacity: 0.5; cursor: not-allowed;` no hover response | | Loading | `data-loading` / prop | Hide content (`visibility: hidden`), absolutely-positioned loader centered | | Selected / Checked | `[aria-selected]`, `[data-state="checked"]` | Fill with `--v-primary-fill`, border with `--v-primary-fill` | | Invalid | `aria-invalid="true"` | Border `var(--v-destructive-fill)`, helper text destructive | **Focus rule:** `:focus` alone fires on mouse clicks too — wrong default. Always `:focus-visible`. Never `outline: none` without a replacement. **Hover rule on touch:** color shifts are OK on touch (read as tap feedback). Transform lifts are NOT — they tap-flicker. Gate transforms with `@media (hover: hover) and (pointer: fine)`. --- ## Responsive — one canonical breakpoint Vireya uses **`768px`** as the primary breakpoint. Below = mobile, above = desktop. Tablet is "small desktop" unless content explicitly demands otherwise. Secondary breakpoints used **sparingly** when a multi-column grid demands a middle tier: - `1024px` — drop 4-col → 2-col - `640px` — drop 2-col → 1-col **Forbidden:** - ❌ Per-component arbitrary breakpoints (`@media (max-width: 819px)`). Pick one of the three. - ❌ Mobile-first `min-width` queries in marketing/landing code (we write desktop-first). ### Touch vs hover gating Use `@media (hover: hover) and (pointer: fine)` to gate hover-only effects (transform lifts, parallax). On touch, those become tap-flickers. ### Charts adapt to their container, not the viewport Charts can sit in narrow columns (sidebar, 2-up grid, dashboard tile). They use `useChartBreakpoint()` from `@vireya/ui/data/chart` and write `data-chart-bp="xs|sm|md"` on the root. CSS reacts via attribute selectors, not viewport queries. --- ## Visual depth — gradients, spotlights, masks Vireya's signature look isn't shadows — it's **soft radial spotlights on top of solid token surfaces**. The treatment is restrained, token-driven, and built from a small set of recipes you compose. Six patterns cover what the official Vireya marketing site uses. ### Universal rules — apply to every gradient 1. **Translucent stops use `color-mix(in srgb, X N%, transparent)` — never `rgba()`.** Tokens have no alpha channel; `color-mix` is the alpha mechanism that stays inside the token system. Banned: `rgba(0,0,0,0.4)`. Required: `color-mix(in srgb, var(--v-primary-fill) 40%, transparent)`. 2. **Always layer the gradient over a solid `var(--v-background-fill)` (or `secondary-fill`)**, never let the gradient be the sole background. If the radial fails to render or the user has a forced theme, you still get a clean surface underneath. 3. **End every spotlight stop at `transparent`** — never end at a solid color. Sharp edges read as bugs. 4. **All colors are tokens.** No `#fff`, no `hsl(...)` literals. Spotlights use `--v-secondary-fill` as the "warm tint"; accent glows use `var(--v-accent-fill, var(--v-primary-fill))` or a scoped `--v-accent-example-*`. 5. **Geometry is proportional (%), not pixel.** Spotlights resize fluidly with the container — no mobile breakpoint needed for the gradient itself. ### Pattern A — Top-spotlight hero (THE Vireya signature) The single most recognizable Vireya treatment. Used on every page hero and showcase header. "Light bleeds from the top centerline." ```css .hero { width: 100%; background: radial-gradient( ellipse 60% 50% at 50% 0%, var(--v-secondary-fill) 0%, transparent 70% ), var(--v-background-fill); border-bottom: 1px solid var(--v-secondary-border); } ``` **Geometry to memorize:** `ellipse 60% 50% at 50% 0%` — 60% wide, 50% tall, centered horizontally, anchored to the top edge. Stop fades from `secondary-fill` to `transparent` at 70%. **Bottom-cap is part of the look.** The `border-bottom: 1px solid var(--v-secondary-border)` is what makes the hero feel anchored. Don't drop it. ### Pattern B — Bottom-spotlight card visual Inverse of A — used inside cards where an illustration sits at the bottom and you want it to "rise from light". A common use is templates card visuals. ```css .cardVisual { background: radial-gradient( ellipse 60% 80% at 50% 100%, var(--v-secondary-fill) 0%, transparent 70% ), var(--v-background-fill); width: 100%; height: 100%; } ``` **Geometry:** `ellipse 60% 80% at 50% 100%` — same width as A, taller (80%), anchored to bottom-center. Pair with Pattern D (bottom-fade mask on the illustration itself) for the canonical "illustration melting into card" effect. ### Pattern C — Accent glow overlay (per-card branded glow) Two-layer composite that paints an accent halo at the top of a card and fades the bottom into the page. Common use: real-world-examples sections that need brand color over neutral screenshots. ```css .glow { position: absolute; inset: 0; z-index: 1; pointer-events: none; background: /* Top: accent halo */ radial-gradient( ellipse 90% 70% at 50% 0%, color-mix(in srgb, var(--v-accent-fill, var(--v-primary-fill)) 30%, transparent) 0%, transparent 60% ), /* Bottom: fade into page bg, helps text legibility */ linear-gradient( to bottom, transparent 55%, color-mix(in srgb, var(--v-background-fill) 65%, transparent) 100% ); } ``` **Rules:** - `pointer-events: none` is mandatory — the glow is decoration, not a click target. - Absolute `inset: 0` + a parent with `position: relative; isolation: isolate` (so `z-index` stacking is local). - The accent stop tops out at **30% opacity** — anything more saturates and reads cheap. - The bottom linear-gradient is what makes white text on the glow stay readable. - Per-instance accent: scope a CSS custom property on a wrapper (`

` defines `--v-accent-example-fill: hsl(...)`), and the glow reads it via `var(--v-accent-example-fill, var(--v-accent-fill, var(--v-primary-fill)))`. **Never set a literal color on the glow itself.** ### Pattern D — Bottom-fade mask (illustration → card edge) Makes an SVG illustration dissolve into the bottom of its container. The mask is purely geometric — no color tokens involved. ```css .illustration { mask-image: linear-gradient(to top, transparent 0%, black 35%); -webkit-mask-image: linear-gradient(to top, transparent 0%, black 35%); } ``` **Geometry:** the bottom **35%** of the element fades to transparent. The number is calibrated — at 25% the illustration looks abruptly cropped; at 50% it loses the focal point. **Always include the `-webkit-mask-image` line** for Safari. The two declarations always come together. Pair with a hover lift to make the illustration feel alive: ```css .card:hover .illustration { transform: translateY(calc(var(--v-height-4) * -1)); } ``` ### Pattern E — Translucent overlay surface (frosted-glass UI) Floating UI element layered over a busy background (screenshot, illustration, gradient). Combines a translucent fill, backdrop blur, and a translucent border. ```css .overlay { background: color-mix(in srgb, var(--v-background-fill) 70%, transparent); backdrop-filter: blur(var(--v-width-8)); -webkit-backdrop-filter: blur(var(--v-width-8)); border: 1px solid color-mix(in srgb, var(--v-secondary-border) 60%, transparent); border-radius: var(--v-radius-lg); padding: var(--v-height-4); } ``` **Recipe:** - Background: 70% of the page fill, 30% transparent — lets the underlying content tint through. - Border: 60% of the standard border token, 40% transparent — keeps the edge visible without going hard. - Blur: `var(--v-width-8)` (16px) is the calibrated default for "noticeable but not smeary". - Always include the `-webkit-backdrop-filter` prefix. ### Pattern F — Halo text-shadow (legibility on busy bg) When text sits over a screenshot, illustration, or any non-uniform background, add a tiny halo built from the page bg color. Reads as legibility aid, not as decorative shadow. ```css .textOverImage { text-shadow: 0 1px 2px color-mix(in srgb, var(--v-background-fill) 80%, transparent); } ``` **Rules:** offset `0 1px`, blur `2px`, color built from `--v-background-fill` at 80%. **Never** use a black shadow over light text — it reads as drop-shadow drama instead of a halo. ### Composition — what makes a Vireya page "feel like Vireya" Stack these three pieces and you've got the canonical look: 1. **Hero with Pattern A** (top-spotlight + bottom-cap border). 2. **Sections with `Section.Root`** (no gradient — solid `--v-background-fill` with the section header underline). 3. **Cards with Pattern B inside the visual area** + Pattern D mask on illustrations + Pattern E for any floating chip/palette/badge over media. The rhythm is: **gradient at the top of the page, neutral middle, gradient inside cards.** Don't gradient every section — the top-spotlight is a punctuation mark, not body type. ### Anti-patterns | Smell | Fix | |---|---| | `background: rgba(0,0,0,0.4)` | `background: color-mix(in srgb, var(--v-primary-fill) 40%, transparent)` | | `background: linear-gradient(...)` with no solid fallback | Layer the gradient OVER `var(--v-background-fill)` | | Spotlight ending at a solid color stop | End every spotlight at `transparent` | | Gradients on every section | Spotlights are punctuation — hero + cards only | | Accent glow at >40% opacity | Cap at 30% — anything more saturates | | Hardcoded `#fff` text-shadow | `color-mix(in srgb, var(--v-background-fill) 80%, transparent)` | | Missing `-webkit-mask-image` / `-webkit-backdrop-filter` | Always pair the prefixed version with the standard one | | Per-card accent set as a literal | Scope it as a CSS custom property on a wrapper class, read via `var(...)` chain | --- ## Composition decisions ### Slots beat configuration when the contained content varies ```tsx // WRONG — closed configuration explodes // RIGHT — open slot New} /> ``` ### `asChild` for polymorphism — never `href` on every component ```tsx // RIGHT // WRONG — forces Button to know about routing ``` ### Compound API rule — simple stays as prop, JSX-rich becomes a child For multi-part components, expose a namespace (``, ``, …). Split rule: > **Simple values stay as props on `Root` (text, enums, booleans, numbers). Interactive or JSX-rich content goes through children + named sub-components.** ```tsx // WRONG — opaque slot props, no IDE structure, forces Fragment ``` Sub-components are identified by `displayName = "FamilyVariant.PartName"` and filtered with `findChild`/`findChildren`/`excludeChildren` (no Context overhead for slot extraction). Use `createContext` only when sub-components share runtime state (e.g. `CTAInlineEmail.Form`'s email input + submit button). ### Compound vs flat — never expose both for the same concern If `Dialog` already exposes `Dialog.Root + .Trigger + .Content + .Header + .Footer`, don't also ship a ``. Pick one API per concern. ### Forward refs and rest props — always `forwardRef + ...props` spread is the contract Vireya follows. When you write your own components, do the same: never filter unknown props — consumers (you, future-self, teammates) add `aria-*`, `data-*`, event handlers you don't know about. --- ## CTA decision matrix — Button variant × surface × size | Role | `variant` | `surface` | `size` | Notes | |---|---|---|---|---| | Hero primary | `accent` (or `primary` if no accent in theme) | `solid` | `lg` | The single highest-emphasis CTA on the page | | Hero secondary | `secondary` | `outlined` | `lg` | Sits next to hero primary, lower emphasis | | Section primary | `primary` | `solid` | `md` | Default in-page CTA inside a section | | Section secondary | `secondary` | `outlined` or `ghosted` | `md` | Adjacent to a primary, or a quiet standalone action | | Card action | `secondary` | `ghosted` | `sm` | Card hover-revealed CTA, "Learn more →" | | Destructive | `destructive` | `solid` | `md` | Pair with `AlertDialog` for confirmation | | Toolbar / table row | `secondary` | `ghosted` | `sm` or `ss` | Dense surfaces — icon + label or icon-only | **Rules:** - **Never two `solid` buttons of the same `variant` in the same group.** One leads (solid), the other demotes to `outlined` or `ghosted`. Two equally-loud CTAs = no CTA. - **Wrap router links via `asChild + ` — never invent an `href` prop on Button.** - **For accent-driven branded moments**, prefer `variant="accent"` over `variant="primary"`. Reserve `primary` solid for the neutral-strong default. --- ## Emphasis decision rule — `accent` or `primary` on blocks The 8 blocks that expose `emphasis: "primary" | "accent"`: `hero/{centered, splitVisual}`, `pricing/{tiers, twoTier}`, `feature/{steps, checkList, highlights}`, `eyebrow`. Default is `"accent"`. **Default to `accent`** for hero, pricing, and feature highlights — promotional surfaces that anchor the page. **Switch to `primary`** when: - The page has no defined accent in the theme (renders identical to primary, but the explicit choice signals intent). - The section is legal/footer/utility chrome that shouldn't draw promotional attention. - You've already used accent in the same viewport-fold. **Rule of thumb: one accent moment per fold.** Two accent CTAs above the fold dilute the brand color. The other 19 blocks intentionally lack `emphasis` because they have no internal element where the choice would have a visible effect (footers/navbars/logo clouds are neutral chrome; CTAs use `variant`). Drive accent there via the `eyebrow` slot — pass `Featured` directly when you need accent. --- ## Self-audit checklist — run before declaring a page done If any check fails, the page is not done. - [ ] **Tokens only**: `grep -RE '#[0-9a-fA-F]{3,8}\b|rgba?\(|\b\d+px\b' src/**/*.module.css` returns nothing inside your section CSS (allowed: `0`, `100%`, `9999px`, `1px`/`1.4px` in inline SVG). - [ ] **Dark theme survives**: toggle to dark theme — no text becomes invisible, no accent CTA disappears (means every accent reference has a `var(--v-accent-*, var(--v-primary-*))` fallback chain). - [ ] **Focus-visible everywhere**: tab through the page — every interactive element shows a visible 2px outline. - [ ] **Server-renderable**: hero, sections, footer all render their content without JS (interactivity may be inert; content must show). - [ ] **Mobile (640px wide)**: section titles shrink (`var(--v-font-size-title) !important`); section padding shrinks to `--v-height-32`; no horizontal scroll. - [ ] **No twin-`solid` CTAs**: in any group, at most one button is `solid` of the lead variant; siblings demote to `outlined` / `ghosted`. - [ ] **No nested blocks**: `@vireya/blocks` blocks never contain other blocks. Sub-sections compose `@vireya/ui` primitives inside `Section.Root`. - [ ] **``** is set in `app/layout.tsx`. - [ ] **`AppProvider` wired** with the full 14-icon map and a locale. - [ ] **No barrel imports**: every `@vireya/ui` and `@vireya/blocks` import is a subpath (the ONE exception is `import { AppProvider } from "@vireya/ui"`). - [ ] **Section.Title pairing**: section titles are `size="title-lg" weight="semibold"`, paired with `` descriptions. - [ ] **One accent moment per viewport-fold**. If two folds both have an accent CTA, demote one to `primary`. --- ## Component pairing — which to pick ### Modal / overlay taxonomy | Component | Use for | |---|---| | `Dialog` | **Blocking decisions** — confirmations, forms that need full attention. Modal backdrop. | | `AlertDialog` | **Destructive confirmations only** — "Delete project?". `Action`/`Cancel` buttons mandatory. | | `Sheet` | **Contextual side panels** — settings, filters, details. Doesn't fully break the flow. | | `Drawer` | **Mobile-style bottom slide-up** — share sheets, filters on touch. (Built on Vaul.) | | `Popover` | **Inline disclosure ≤300px** — color picker, quick form, contextual actions. Anchored to trigger. | | `HoverCard` | **Preview on hover ≤1 sentence** — user mention preview, link metadata. Never actionable content. | | `Tooltip` | **≤1 sentence on hover only** — clarification, keyboard hint. Never actionable, never essential info. | | `DropdownMenu` | **List of actions/links** — context menu, "more" menu. Items are commands. | | `ContextMenu` | **Right-click menu** — same as dropdown but trigger is `contextmenu` event. | | `Toast` | **Async feedback ≤3 lines** — "Saved", "Connection lost". Auto-dismisses. Non-blocking. | **Rules:** - Anything actionable → `Popover` (anchored) or `DropdownMenu` (list of actions). NEVER `Tooltip` or `HoverCard`. - Confirmations destructive → `AlertDialog`. Confirmations non-destructive → `Dialog`. - Filter/settings on desktop → `Sheet`. Same on mobile → `Drawer`. ### Page section taxonomy — `@vireya/blocks` are FULL sections | Layer | Role | |---|---| | `@vireya/blocks` | Full landing-page sections (Hero, Pricing, FAQ, Footer). One per page-region. | | `@vireya/ui` | Primitives (Button, Card, Input, Tab). Compose into custom sections when no block fits. | **Rules:** - Never nest a block inside another block. If a feature section needs a sub-section, compose `@vireya/ui` primitives directly. - Never re-implement a block as primitives if the block exists. `Hero.Centered` already handles eyebrow + title + description + actions + responsive — don't rebuild it. - Always pass `eyebrow` as a `ReactNode` slot (use the `Eyebrow` primitive, not a custom span). Block props like `eyebrow?: ReactNode` accept rich nodes. ### `emphasis` prop — defaults to `accent` Blocks with an `emphasis: "primary" | "accent"` prop default to `"accent"`. Change to `"primary"` only when: - The eyebrow text **is** a CTA label (rare) - The page has no defined accent and you want to be explicit - A neutral/legal/footer section that shouldn't draw promotional attention --- ## Block-scoped tokens `@vireya/blocks` follows a **two-layer token model**: blocks consume design system tokens (`--v-*`), but expose their own scoped surface (`--v-block-{family}-{property}`) for per-block customization. ### When to add a block-scoped token Tokenize **identity-defining** properties — values where a brand or consumer would reasonably want to differ: - ✅ Surface colors (card bg, section bg) - ✅ Border colors - ✅ Accent colors (highlights, indicators) - ✅ Decorative properties unique to the block (overlay color, grayscale opacity) - ❌ Spacing, padding (use design system tokens directly) - ❌ Layout properties (grid, flex) - ❌ Typography weight/size (already tokenized globally) ### Defaults must be design system tokens ```css .section { /* RIGHT — falls back to active theme out of the box */ --v-block-hero-accent: var(--v-primary-fill); /* WRONG — locks the block to a literal */ --v-block-hero-accent: #00aa55; } ``` Override at any cascade level: per-instance via `style`, per-page via wrapper class, per-theme via `:root.v-theme-brand body { ... }`. --- ## Accessibility floors These are minimums. Going below is a bug. - **Color contrast:** WCAG AA (4.5:1 body, 3:1 large text). Default themes meet this; verify custom themes before shipping. - **Focus indicators:** every interactive element needs a visible `:focus-visible` ring. - **Keyboard:** every interactive element reachable + operable via keyboard. No mouse-only handlers. - **Hit targets:** primary CTAs ≥ 44×44px (use `lg`/`xl` field size). Inline icon buttons may go ≥ 32px in dense contexts. - **ARIA:** primitives from `@base-ui-components/react` and Radix come with correct ARIA — don't strip it. Propagate `aria-*` props explicitly. - **Semantic HTML:** `` | | ` ``` **Rules:** - Never two `variant="primary"` buttons. Promote one (the actual decision), demote the other to `secondary` + `surface="outlined"` or `surface="ghosted"`. - For accent-driven CTAs (promotional / branded moments), use `variant="accent"` instead of primary. - Always wrap router links via `asChild + ` — never invent an `href` prop on Button. --- ## 5. Pricing tier with a custom CTA per tier ```tsx "use client"; import { PricingTiers } from "@vireya/blocks/pricing/tiers"; import { Button } from "@vireya/ui/form/button"; import Link from "next/link"; Core features Community support Everything in Starter Priority support ``` **Notes:** - Anything that isn't a `PricingTiers.Feature` becomes the tier's **action slot** — typically your CTA button. - The highlighted tier auto-receives Root's `emphasis` (defaults to `accent`); pass `emphasis="primary"` on Root to mute the brand color. - Use `highlighted` + `highlightedLabel` (NOT `popular`). --- ## 6. Install snippet — package-manager tabs + Shiki-rendered code The install snippet section is server-side syntax-highlighted with Shiki, with a tiny client component for the copy interaction. ```ts // app/lib/highlight.ts (or similar — server-only) import { codeToHtml } from "shiki"; const cache = new Map>(); export function highlightTsx(source: string) { const cached = cache.get(source); if (cached) return cached; const promise = codeToHtml(source, { lang: "tsx", themes: { light: "github-light", dark: "github-dark" }, defaultColor: false, }); cache.set(source, promise); return promise; } ``` ```tsx // app/components/install.tsx (server async) import { TabsRoot, TabsList, TabsTrigger, TabsContent } from "@vireya/ui/data/tab"; import { highlightTsx } from "../lib/highlight"; import { CopyButton } from "./copy-button"; export const InstallSection = async () => { const html = await highlightTsx(`import { Button } from "@vireya/ui/form/button";\nexport default () => ;`); const commands = [ { id: "pnpm", label: "pnpm", command: "pnpm add @vireya/ui" }, { id: "npm", label: "npm", command: "npm install @vireya/ui" }, { id: "yarn", label: "yarn", command: "yarn add @vireya/ui" }, ]; return ( {commands.map((c) => {c.label})} {commands.map((c) => ( $ {c.command} ))}
); }; ``` ```tsx // app/components/copy-button.tsx "use client"; import { useState } from "react"; import { Check, Copy } from "lucide-react"; export const CopyButton = ({ value }: { value: string }) => { const [copied, setCopied] = useState(false); return ( ); }; ``` **Why this split:** Shiki is server-only (large bundle, Node-only). The async server section pre-renders the HTML; the copy button is a tiny client island. Same approach for any "code preview" block. --- ## 7. Stats bar (derived counts) A thin, dense bar right after hero. Counts derive from data sources you already have (manifests, package.json, env), not hardcoded. ```tsx import { manifest as blocksManifest } from "@vireya/blocks/showcase"; export const StatsSection = () => { const items = [ { label: "Components", value: "120+" }, { label: "Blocks", value: `${blocksManifest.length}` }, { label: "License", value: "MIT + Commercial" }, ]; return (
{items.map((item) => (
{item.label}
{item.value}
))}
); }; ``` ```css .bar { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--v-secondary-border); border-radius: var(--v-radius-xl); background: var(--v-background-fill); overflow: hidden; } .cell { display: flex; flex-direction: column; gap: var(--v-height-4); padding: var(--v-height-20) var(--v-width-24); border-left: 1px solid var(--v-secondary-border); } .cell:first-child { border-left: none; } @media (max-width: 720px) { .bar { grid-template-columns: repeat(2, 1fr); } } ``` --- ## 8. One-off branded accent zone (without polluting the global accent) Sometimes a single page (landing, marketing, illustration) wants a distinct accent that shouldn't bleed into the rest of the app. Don't override `--v-accent-*` globally — scope a parallel tier under a class. ```css /* globals.css */ .landing { /* Landing-page-only example accent. Used by illustrations and example surfaces; never read by core Vireya components. */ --v-accent-example-fill: hsl(150 80% 38%); --v-accent-example-foreground: hsl(0 0% 98%); --v-accent-example-border: hsl(150 50% 24%); } ``` ```tsx
{/* sections inside can read var(--v-accent-example-fill, var(--v-accent-fill, var(--v-primary-fill))) */}
``` **Why a custom prefix (`--v-accent-example-*`) and not `--v-accent-*` directly:** - Scoped to the landing class only — the rest of the app keeps whatever accent is set globally (or no accent at all). - Doesn't break the cross-app `accent → primary` fallback contract. - Easy to grep and remove later when the section hierarchy changes. --- ## 9. RSC + namespace block exports — when to mark `"use client"` Vireya blocks expose a namespace object (`PricingTiers.Root`, `ContactSplit.Root`, `FAQTwoColumn.Root`). When that namespace is **defined inside a `"use client"` module** (because the block uses `createContext` or browser APIs), React's RSC layer rewrites the namespace into a client reference. Property access on a client reference from a Server Component returns `undefined` — so `PricingTiers.Root` becomes `undefined` and you get **"Element type is invalid... got: undefined"** at render. **Fix:** mark the consumer section as `"use client"`. This puts both the consumer and the block's chunk in the same client boundary; namespace property access works normally. ```tsx "use client"; // <- required, even for sections without state of their own import { PricingTiers } from "@vireya/blocks/pricing/tiers"; export const PricingSection = () => ( ... ); ``` Rule of thumb: if the block uses `createContext` internally, your consumer section needs `"use client"`. When in doubt, just add it — the cost is one extra client component, the benefit is no mysterious render crash. --- ## 10. Catalog rail (illustration-first preview cards) A rail-style preview of a category (blocks family, chart family, etc.). Illustration on top fills the visible area; meta below stays compact. ```tsx
{/* large SVG illustration, not a small icon */}
{title} {count} variants
``` ```css .familyCard { display: flex; flex-direction: column; border: 1px solid var(--v-secondary-border); border-radius: var(--v-radius-xl); background: var(--v-background-fill); overflow: hidden; text-decoration: none; color: inherit; transition: border-color var(--v-motion-duration) var(--v-motion-timing), transform var(--v-motion-duration) var(--v-motion-timing); } .familyCard:hover { border-color: var(--v-secondary-active); transform: translateY(-2px); } .familyVisual { aspect-ratio: 200 / 120; /* match the SVG viewBox */ background: var(--v-background-fill); border-bottom: 1px solid var(--v-secondary-border); display: flex; align-items: flex-end; justify-content: center; overflow: hidden; } .familyIllustration { width: 100%; aspect-ratio: 200 / 120; /* Fades the illustration into the bottom edge — feels less "cropped". */ mask-image: linear-gradient(to top, transparent 0%, black 35%); -webkit-mask-image: linear-gradient(to top, transparent 0%, black 35%); } .familyIllustration > svg { display: block; width: 100%; height: 100%; } .familyMeta { display: flex; align-items: center; justify-content: space-between; padding: var(--v-height-12) var(--v-width-14); } ``` **Key:** the visual `aspect-ratio` MUST match the inner SVG's `viewBox` ratio, otherwise you get letterboxing or cropping. If your SVG is `viewBox="0 0 200 120"`, the container is `aspect-ratio: 200 / 120` (or `5 / 3`). --- ## 11. Theme editor / "copy theme JSON" close-out After an interactive theme demo, give the user a **next step**: copy the current state as code they can paste into `createTheme()`. ```tsx "use client"; import { Button } from "@vireya/ui/form/button"; import { Check, Copy } from "lucide-react"; const [tokens, setTokens] = useState(initialTokens); const [copied, setCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(JSON.stringify(tokens, null, 2)); setCopied(true); setTimeout(() => setCopied(false), 1600); }; ``` Pattern applies to anything interactive — **always close the loop with an actionable artifact** (copy, download, email me) instead of leaving the user at a dead end. --- ## 12. Anchor IDs — section navigation that just works Sections that may be linked from elsewhere on the page (or external) get an `id` matching the URL fragment. CTAs use `href="#pricing"` etc. ```tsx ... ... ... ``` The Section primitive's `scroll-margin-top: var(--v-height-32)` keeps the anchored target from sliding under a sticky header. If your header is taller, set `scroll-margin-top` to the header height + a comfortable gap on `Section.Root`. --- # File: references/setup.md # Next.js setup with Vireya Copy-pasteable templates below — drop them into a fresh Next.js app and you're done. --- ## Install ```bash pnpm add @vireya/core @vireya/ui @vireya/next react@19.2.1 react-dom@19.2.1 next pnpm add lucide-react # required: AppProvider needs icons ``` Optional peers — install only when you use the corresponding component: - `@tanstack/react-table` — for `@vireya/ui/data/dataTable` - `recharts` — for `@vireya/ui/data/chart` - `embla-carousel-react` — for `@vireya/ui/data/carousel` - `react-day-picker` + `date-fns` — for `@vireya/ui/data/calendar` and `@vireya/ui/form/dateField` - `cmdk` — for `@vireya/ui/data/command` - `vaul` — for `@vireya/ui/overlay/drawer` - `react-resizable-panels` — for `@vireya/ui/layout/resizable` - `@headless-tree/core` — for `@vireya/ui/navigation/treeView` - `react-hook-form` — if you use the form helpers If you also use blocks: `pnpm add @vireya/blocks`. --- ## File 1 — `app/layout.tsx` (server component) ```tsx import { ThemeProvider } from "@vireya/next"; import "@vireya/next/styles"; // 1st: base tokens + light/dark themes import "@vireya/next/motion/wipe-reveal"; // optional: View Transitions preset import "../styles/globals.css"; // last: app overrides import { createTheme } from "@vireya/core"; import { AppProvider } from "./layout.client"; import type { PropsWithChildren } from "react"; const lightBrand = createTheme({ background: "hsl(0 0% 98%)", secondary: "hsl(0 0% 92%)", primary: "hsl(0 0% 8%)", accent: "hsl(150 80% 38%)", // optional brand color }); const darkBrand = createTheme({ background: "hsl(0 0% 0%)", secondary: "hsl(0 0% 14%)", primary: "hsl(0 0% 96%)", accent: "hsl(150 80% 45%)", }); export default function RootLayout({ children }: PropsWithChildren) { return ( {children} ); } ``` **Hard rules:** - `@vireya/next/styles` import must be **first** so app overrides win. - `` is **required** — the theme bootstrap script runs before React hydrates and would otherwise trigger a mismatch. - Motion preset import is a **CSS side-effect** — there's no JS export. Pick at most one of `wipe-reveal`, `mask-reveal`, `circle-reveal`, `angled-reveal`. --- ## File 2 — `app/layout.client.tsx` (client wrapper) ```tsx "use client"; import { AppProvider as UIAppProvider } from "@vireya/ui"; import ptBR from "@vireya/ui/locales/ptBR"; // or enUS import { Brush, Calendar, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, X as Close, CloudUpload, Copy, Eye, EyeOff, Search, } from "lucide-react"; import type { PropsWithChildren } from "react"; export const AppProvider = ({ children }: PropsWithChildren) => ( {children} ); ``` **Why every name is required:** Vireya components reference these icons by canonical name. Missing any one of them silently breaks the corresponding component: | Icon | Used by | |---|---| | `Calendar` | `Calendar`, `DateField` trigger | | `Check` | `Checkbox` indicator, `DropdownMenu`/`ContextMenu` checkbox items, `Command` selection, `CopyField` after copy | | `ChevronDown`/`Up`/`Left`/`Right` | `Select` indicator, `Calendar` nav, `Sidebar`/`TreeView` expanders, scroll arrows | | `Circle` | `RadioGroup` indicator, `DropdownMenu`/`ContextMenu` radio items | | `Close` (`X`) | `Toast` close, `Dialog`/`Drawer`/`Sheet` close buttons | | `CloudUpload` | `UploadArea` default icon | | `Copy` | `CopyField` | | `Eye`/`EyeOff` | `PasswordField` toggle | | `Search` | `Command` input, `DataTable` search | | `Brush` | theme/style indicators in showcases | Substitute any icon library that exports React components with the same shape (e.g. `phosphor-react`) — the contract is just a `React.ComponentType`. `AppProvider` is the **one allowed root import** from `@vireya/ui`. Everything else is a subpath. --- ## File 3 — `styles/globals.css` ```css * { box-sizing: border-box; margin: 0; padding: 0; } body { --v-page-max-width: 1520px; --v-page-padding: var(--v-width-16); /* or: clamp(16px, 5vw, 32px) for fluid gutters */ } ``` `--v-page-max-width` and `--v-page-padding` are **not** shipped by `@vireya/core` but `@vireya/blocks` assumes they exist. Define them here. --- ## Theme switching (client) ```tsx "use client"; import { useTheme } from "@vireya/next/client"; export function ThemeToggle() { const { theme, setTheme, themes } = useTheme(); return ( ); } ``` `useTheme()` returns `{ theme, setTheme, themes, forcedTheme }`. The hook: - persists the choice via cookie (`cookieName` from ``, default `"theme"`) - applies `v-theme-{name}` class on `` - respects `prefers-reduced-motion: reduce` (skips View Transitions animation) - safe to call from any client component below `` --- ## Custom theme — `createTheme` ```ts import { createTheme } from "@vireya/core"; const myTheme = createTheme({ background: "hsl(240 6% 97%)", secondary: "hsl(240 6% 92%)", primary: "hsl(240 8% 13%)", // Optional — falls back to defaults if omitted: destructive: "hsl(0 84% 60%)", link: "hsl(221 83% 53%)", // Optional — without it, --v-accent-* is undefined; consumers must use // var(--v-accent-fill, var(--v-primary-fill)) fallbacks (they should already). accent: "hsl(150 80% 38%)", // Per-subtoken override (anything you don't override is auto-derived): // primary: { fill: "hsl(240 8% 13%)", hover: "#4d4d5b" } }); myTheme.toCss("v"); // → "color-scheme:light;--v-background-fill:#...;..." myTheme.raw; // → { background: { fill, foreground, ... }, ... } myTheme.colorScheme; // → "light" | "dark" (auto from background luminance) myTheme.extend({ accent: "#ff00aa" }); // returns a new theme ``` Pass to ``. The default `light` and `dark` themes are always present; you don't need to redefine them. --- ## Motion presets (route + theme transitions) Each preset is a CSS file imported as a side-effect from `@vireya/next/motion/`. Only the `::view-transition-*` pseudos are styled — the actual animation triggers when the browser starts a View Transition (theme switch via `useTheme().setTheme`, or Next.js App Router navigation when you set `viewTransitionName`). | Preset | Effect | Best for | |---|---|---| | `wipe-reveal` | diagonal clip-path wipe (~150ms) | fast page transitions, default pick | | `circle-reveal` | circular expand from `(--v-transition-start-x, --v-transition-start-y)` | theme toggle from a click point (pass `{ x, y }` to `setTheme`) | | `mask-reveal` | radial mask reveal using an image (default: a Tenor GIF; override via `--v-gif-mask-url`) | playful brand-driven transitions | | `angled-reveal` | angled diagonal slide (1s) | stylized hero transitions | You can import multiple — they target the same pseudo-elements, so the **last one wins** for any given selector. In practice, pick one. To trigger a route transition with the preset: ```tsx "use client"; import Link from "next/link"; export const NavLink = (props: React.ComponentProps) => ( ); ``` --- ## Verification checklist for a new app After scaffolding, confirm: - [ ] `` present - [ ] `@vireya/next/styles` imported **first** in layout - [ ] `` wraps `` wraps `{children}` - [ ] `AppProvider` passes both `locale` and the full `icons` map (14 names above) - [ ] `globals.css` defines `--v-page-padding` and `--v-page-max-width` - [ ] No hardcoded colors/sizes anywhere — only `var(--v-*)` - [ ] All component imports are subpaths: `@vireya/ui//` or `@vireya/blocks//` - [ ] `useTheme` is imported from `@vireya/next/client`, not `@vireya/next` --- ## Next: build the canonical Section primitive After wiring providers, your first app component should be the `Section` namespace primitive (Root/Header/Eyebrow/Title/Description). Every Vireya page section composes it. The full code lives in `references/recipes.md` recipe #1 — copy it verbatim into `src/components/section.tsx` + `section.module.css`. The pairing is calibrated: `` for `Section.Title`, `` for `Section.Description`, mobile shrinks the title to `var(--v-font-size-title) !important`. --- # File: references/blocks.md # Vireya blocks — compound API reference Pre-composed landing-page sections in `@vireya/blocks`. All blocks use a compound API (`` + items). Common props across families: `eyebrow`, `title`, `description` (all `ReactNode`); `variant: "default" | "shaded"` for background variants where applicable. For prop details beyond what's listed here, open the DTS at `node_modules/@vireya/blocks/dist/components///index.d.ts` (or use IDE go-to-definition on the import). --- ## Which blocks expose `emphasis: "primary" | "accent"` `emphasis` defaults to `"accent"`. **Only the 8 blocks below expose it** — the rest are intentionally neutral chrome. - `hero/centered` — `HeroCentered` - `hero/splitVisual` — `HeroSplitVisual` - `pricing/tiers` — `PricingTiers` - `pricing/twoTier` — `PricingTwoTier` - `feature/steps` — `FeatureSteps` - `feature/checkList` — `FeatureCheckList` - `feature/highlights` — `FeatureHighlights` - `eyebrow` — `Eyebrow` **For the other 19 blocks** (footers, navbars, logo clouds, FAQ, CTA banners, contact, hero-minimal, hero-backgroundImage, testimonials, etc.), drive accent through the `eyebrow` slot — pass `Featured` directly when you need accent there. ### When to flip emphasis to `primary` Default to `accent` for promotional surfaces (hero, pricing, feature highlights). Switch to `primary` when: - The page has no defined accent in the theme. - The section is legal/footer/utility chrome that shouldn't draw promotional attention. - You've already used accent in the same viewport-fold (one accent moment per fold). --- ## navbar ### `@vireya/blocks/navbar/simple` — `NavbarSimple` - `Root`: `position?: "sticky" | "static"` (default `"sticky"`) - `Logo`, `Link` (`href`, `children`), `Actions` (right-side button slot) ### `@vireya/blocks/navbar/dropdown` — `NavbarDropdown` - `Root`: `position?: "sticky" | "static"` - `Logo`, `Link`, `Actions` - `Dropdown`: `label: ReactNode`, `columns?: number` — opens a multi-column menu - `MenuLink` (inside `Dropdown`): `href`, `description?`, `icon?`, `children` (title) --- ## hero All four accept `eyebrow?`, `title` (required), `description?`, `className?`, `children?` (action buttons), and extend `HTMLAttributes`. ### `@vireya/blocks/hero/centered` — `HeroCentered` - Extra: `badge?`, `align?: "center" | "left"` (default `"center"`), `emphasis?: "primary" | "accent"` ### `@vireya/blocks/hero/minimal` — `HeroMinimal` - No badge, no emphasis. Just eyebrow → title → description → actions. ### `@vireya/blocks/hero/backgroundImage` — `HeroBackgroundImage` - Extra: `imageSrc` (required), `imageAlt?`, `overlay?: number` (0–1, default `0.5`), `badge?`, `align?` ### `@vireya/blocks/hero/splitVisual` — `HeroSplitVisual` - Extra: `badge?`, `reverse?: boolean` (swap text/visual sides), `emphasis?` - Subcomponent: `Visual` — slot for the right-side (or left-side if `reverse`) media --- ## logoCloud ### `@vireya/blocks/logoCloud/grid` — `LogoCloudGrid` - `Root`: `eyebrow?`, `title?`, `columns?: 3 | 4 | 5 | 6` (default `4`), `bordered?` (default `true`), `grayscale?` (default `true`) - `Logo`: `src` (required), `alt` (required), `href?` ### `@vireya/blocks/logoCloud/row` — `LogoCloudRow` - `Root`: `eyebrow?`, `title?`, `grayscale?` (default `true`) - `Logo`: same as above --- ## feature ### `@vireya/blocks/feature/alternating` — `FeatureAlternating` - `Root`: `eyebrow?`, `title?`, `description?` - `Row`: `eyebrow?`, `title` (required), `description?` — alternates left/right automatically - `Visual`: media slot inside each `Row` ### `@vireya/blocks/feature/checkList` — `FeatureCheckList` - `Root`: `eyebrow?`, `title?`, `description?`, `icon?: ReactNode` (custom check icon, defaults to a built-in), `columns?: 1 | 2` (default `1`), `emphasis?: "primary" | "accent"` - `Item`: `title` (required), `description?` ### `@vireya/blocks/feature/highlights` — `FeatureHighlights` - `Root`: `eyebrow?`, `title?`, `description?`, `columns?: 2 | 3 | 4` (default `4`), `emphasis?` - `Item`: `icon?`, `title` (required), `description` (required) ### `@vireya/blocks/feature/iconGrid` — `FeatureIconGrid` - `Root`: `eyebrow?`, `title?`, `description?`, `columns?: 2 | 3 | 4` (default `3`) - `Item`: `icon?`, `title` (required), `description?` ### `@vireya/blocks/feature/steps` — `FeatureSteps` - `Root`: `eyebrow?`, `title?`, `description?`, `emphasis?` - `Item`: `number?`, `title` (required), `description?`, `action?` (per-step CTA slot) --- ## pricing ### `@vireya/blocks/pricing/tiers` — `PricingTiers` - `Root`: `eyebrow?`, `title?`, `description?`, `emphasis?: "primary" | "accent"` - `Tier`: `name` (required), `price` (required), `period?`, `description?`, `highlighted?: boolean` (highlights with accent ring + badge), `highlightedLabel?: ReactNode` (defaults to `"Most popular"`) - `Feature`: checklist row inside a tier — pass children as the feature label ### `@vireya/blocks/pricing/twoTier` — `PricingTwoTier` - `Root`: same as `tiers` - `Tier`: same shape (`name`, `price`, `period?`, `description?`, `highlighted?: boolean`) - `Feature`: same --- ## cta ### `@vireya/blocks/cta/banner` — `CTABanner` - `eyebrow?`, `title` (required), `description?`, `variant?: "default" | "shaded"` (default `"default"`), `children` (action buttons) ### `@vireya/blocks/cta/centered` — `CTACentered` - Same shape as `banner`, centered layout. ### `@vireya/blocks/cta/inlineEmail` — `CTAInlineEmail` - `Root`: `eyebrow?`, `title` (required), `description?`, `disclaimer?`, `variant?: "default" | "shaded"` (default `"shaded"`) - `Form`: `onSubmit?: (email: string) => void | Promise` — manages internal state - `Input`: `placeholder?: string` (default `"you@example.com"`) - `Submit`: extends `Button` props --- ## faq ### `@vireya/blocks/faq/accordion` — `FAQAccordion` - `Root`: `eyebrow?`, `title?`, `description?`, `openMultiple?: boolean` (default `false`), `surface?: "outlined" | "ghosted"` (default `"outlined"`) - `Item`: `question` (required), `children` = answer ### `@vireya/blocks/faq/twoColumn` — `FAQTwoColumn` - `Root`: `eyebrow?`, `title` (required), `description?`, `openMultiple?`, `surface?: "outlined" | "ghosted"` (default `"ghosted"`) - `Item`: same as above - `Aside`: left-side content slot (e.g. extra description, contact CTA) --- ## testimonials ### `@vireya/blocks/testimonials/single` — `TestimonialsSingle` - `eyebrow?`, `logo?`, `quote` (required), `author` (required), `role?`, `avatar?`, `variant?: "default" | "shaded"` (default `"default"`) ### `@vireya/blocks/testimonials/grid` — `TestimonialsGrid` - `Root`: `eyebrow?`, `title?`, `description?`, `columns?: 2 | 3` (default `3`) - `Card`: `quote` (required), `author` (required), `role?`, `avatar?` --- ## contact ### `@vireya/blocks/contact/simple` — `ContactSimple` - `Root`: `eyebrow?`, `title` (required), `description?`, `disclaimer?`, `variant?: "default" | "shaded"` - `Form`: `onSubmit?: (values: { name, email, message }) => void | Promise` - `Name`, `Email`, `Message`, `Submit`: pre-styled fields wired to the form's internal state ### `@vireya/blocks/contact/split` — `ContactSplit` - `Root`: `eyebrow?`, `title` (required), `description?` - `Info` (left): container for `InfoItem` rows - `InfoItem`: `icon?`, `label` (required), `value` (required), `href?` (turns `value` into a link) - `Form` (right): `onSubmit?: (values: { name, email, subject, message }) => void | Promise` - `FieldRow`: groups `Name`/`Email` side-by-side - `Name`, `Email`, `Subject`, `Message`, `Submit`: wired fields --- ## footer ### `@vireya/blocks/footer/minimal` — `FooterMinimal` - `Root`, `Brand` (logo slot), `Link` (`href`, `children`), `Trailing` (right-side slot for socials/legal) ### `@vireya/blocks/footer/columns` — `FooterColumns` - `Root`, `Brand` - `Column`: `title` (required), `children` = `Link` items - `Link`: `href`, `children` - `Bottom`: footer-bottom slot (copyright, legal links) --- ## eyebrow (utility, also reused inside other blocks) ### `@vireya/blocks/eyebrow` — `Eyebrow`, `EyebrowGroup` - `Eyebrow`: `variant?: "plain" | "muted" | "pill" | "dot"` (default `"plain"`), `size?: number` (default `12`), `weight?: "regular" | "medium" | "semibold" | "bold"` (default `"medium"`), `emphasis?: "primary" | "accent"` (default `"accent"`), `children` (required) - `EyebrowGroup`: container with alignment support When a block accepts an `eyebrow?: ReactNode` prop, you can pass either a plain string or a fully-composed `Featured`. --- ## Patterns - **Variant dedup:** if two showcases of a block differ only by an opt-in prop (e.g. `popular`, `bordered`), don't duplicate them. The block already supports both. - **Emphasis prop:** defaults to `"accent"`. Pass `"primary"` for sections that should not draw promotional attention (footer, contact, neutral CTAs). - **`variant: "shaded"`:** uses `--v-secondary-fill` as background — useful to break visual rhythm between sections without leaving the token system. - **Children-as-actions:** most blocks treat `children` as the action button slot. Use `` to wire to Next.js routing. --- # File: references/tokens.md # Vireya tokens — full catalog All tokens are CSS custom properties prefixed `--v-*`, declared on `body` by `@vireya/next/styles`. Inspect them at runtime in DevTools (Computed → Variables) or read the built CSS at `node_modules/@vireya/next/dist/styles.css`. --- ## Base units (overridable via ``) ``` --v-height 2px base unit for graded --v-height-N --v-width 2px base unit for graded --v-width-N --v-motion-delay 0ms --v-motion-duration 250ms alias of --v-motion-default --v-motion-timing cubic-bezier(0.4, 0, 0.2, 1) --v-overlay-z-index 9 --v-w #ffffff "white" anchor (theme-invariant) --v-b #0a0a0a "black" anchor (theme-invariant, NOT pure black) ``` `--v-w` and `--v-b` are foreground anchors used by auto-derived `foreground` sub-tokens. Override them at the root if your app needs a different "light text" / "dark text" pair. > **Removed in 0.1.0:** the bare `--v-font` and `--v-radius-md` base units (and the graded `--v-font-N`, `--v-radius-N` derived from them). Font sizes and radii are now semantic, not graded. --- ## Font families & weights ``` --v-font-family-sans Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif --v-font-family-mono ui-monospace, "SF Mono", Menlo, "Cascadia Code", "JetBrains Mono", Consolas, monospace --v-font-weight-regular 400 body text --v-font-weight-medium 500 UI active, subheads, buttons, badges --v-font-weight-semibold 600 display, H2/H3 ≥24px, section headings --v-font-weight-bold 700 art-direction only (rare) ``` --- ## Type ramp (semantic) `--v-font-size-{name}` — 9 degraus modulares. Use os nomes semânticos via `` ou direto em CSS. | Token | px | Role | |---|---|---| | `--v-font-size-caption` | 12 | Eyebrow, caption, badge, micro-label | | `--v-font-size-body-sm` | 14 | Small body, labels, table cells, helpers | | `--v-font-size-body` | 16 | **Body default** — paragraphs, list items, form inputs | | `--v-font-size-body-lg` | 18 | Lead paragraph | | `--v-font-size-title-sm` | 20 | Card title, subhead | | `--v-font-size-title` | 24 | Section subtitle, H3 | | `--v-font-size-title-lg` | 32 | H2, dashboard heading | | `--v-font-size-display` | 48 | Hero medium, pricing display | | `--v-font-size-display-lg` | 64 | Hero principal | Em React: ``. Em CSS: `font-size: var(--v-font-size-body);`. --- ## Line-height & letter-spacing ``` --v-leading-tight 1.05 display --v-leading-snug 1.15 titles --v-leading-normal 1.4 UI / labels / dense --v-leading-relaxed 1.55 body paragraph --v-tracking-tight -0.02em display --v-tracking-normal 0 default --v-tracking-wide 0.04em UI labels small caps --v-tracking-eyebrow 0.08em uppercase eyebrows ``` Em CSS: `line-height: var(--v-leading-relaxed);`, `letter-spacing: var(--v-tracking-eyebrow);`. Sem literais inline. --- ## Spacing scale (graded) `--v-width-N` (horizontal) e `--v-height-N` (vertical). Mesma escala numérica em ambos: ``` 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 32, 40, 48, 64, 80, 96, 128 ``` 20 valores. Faixa principal step=2 (2..24), depois saltos maiores (32, 40, 48, 64, 80, 96, 128) + hairline `1`. Common usage: - `1`–`8`: internal component spacing (icon gap, label-to-input) - `12`: tight vertical (eyebrow → title) - `16`: standard inline gap - `24`: standard vertical group gap - `32`: major group gap (title → actions) - `40`: field/button height (default) - `48`–`64`: section-internal gaps - `80`: mid gutter - `96`–`128`: section padding (mobile / desktop hero) > **Removidos** em 0.1.0: `3, 5, 28, 38, 42` (valores arbitrários). Snap pra vizinho mais próximo via codemod. --- ## Radius scale (semantic) `--v-radius-{name}` — 7 degraus, sem furos numéricos. Substitui a antiga escala graded `--v-radius-N`. | Token | px | Use | |---|---|---| | `--v-radius-xs` | 2 | Chart bars, hairline accents | | `--v-radius-sm` | 4 | Badges, chips, tags, tooltip | | `--v-radius-md` | 6 | Inputs, buttons (default), alerts, dropdown | | `--v-radius-lg` | 8 | Buttons large, button `shape_square` | | `--v-radius-xl` | 12 | Cards, feature items, dialog | | `--v-radius-2xl` | 16 | Pricing tier, hero card, drawer | | `--v-radius-full` | 9999px | Pill / circle (intencional) | --- ## Composite tokens (component sizing — used inside CSS Modules) Five size tiers — `ss, sm, md, lg, xl` — pre-composed from the graded scales. Use these instead of recomposing manually. ### Field (button, input, select, etc.) | Tier | height | padding | font | size (inner) | |---|---|---|---|---| | `ss` | 24px | 12px | `caption` (12) | 20px | | `sm` | 32px | 12px | `body-sm` (14) | 24px | | `md` (default) | 40px | 16px | `body-sm` (14) | 24px | | `lg` | 40px | 20px | `body` (16) | 32px | | `xl` | 48px | 24px | `body` (16) | 40px | ```css height: var(--v-field-md-height); padding-inline: var(--v-field-md-padding); font-size: var(--v-field-md-font); ``` ### Avatar | Tier | size | font | |---|---|---| | `ss` | 24px | `caption` (12) | | `sm` | 32px | `caption` (12) | | `md` | 48px | `body` (16) | | `lg` | 64px | `title` (24) | | `xl` | 96px | `title-lg` (32) | ### Badge | Tier | font | padding | |---|---|---| | `ss` | `caption` (12) | 8px | | `sm` | `caption` (12) | 12px | | `md` | `body-sm` (14) | 12px | | `lg` | `body` (16) | 16px | | `xl` | `body-lg` (18) | 16px | ### Icon | Tier | size | |---|---| | `ss` | 4px | | `sm` | 8px | | `md` | 12px | | `lg` | 16px | | `xl` | 20px | (Icon size composite is intentionally small because icons usually sit inside fields/buttons; for standalone icons use `--v-width-N` directly.) --- ## Color sub-token model Every named color expands to **6 sub-tokens** via `parseColor()`: | Sub-token | Purpose | Auto-derivation if not overridden | |---|---|---| | `fill` | base color (button bg, badge fill, primary text) | required input | | `foreground` | text/icon on top of `fill` | `var(--v-b)` if fill is light, else `var(--v-w)` | | `hover` | `:hover` state | `safeLighten(fill, +20%)` | | `active` | `:active` / `[aria-selected]` | `safeLighten(fill, +40%)` | | `subdued` | de-emphasized (disabled bg, muted text) | `safeLighten(fill, +50%)` | | `border` | subtle border on a fill of this color | `safeLighten(fill, +8%)` | **Always pair them correctly:** ```css .solid { background-color: var(--v-primary-fill); color: var(--v-primary-foreground); border: 1px solid var(--v-primary-border); } .solid:not(:disabled):hover { background-color: var(--v-primary-hover); } .solid:not(:disabled):active { background-color: var(--v-primary-active); } .solid[aria-disabled="true"] { background-color: var(--v-primary-subdued); } ``` ### Color names | Name | Required? | Use | Notes | |---|---|---|---| | `background` | yes | page bg, neutral surfaces | | | `secondary` | yes | subtle surfaces, borders, muted fills, disabled | | | `primary` | yes | foreground text, neutral CTAs, default icons/borders | neutral-strong (near-black/white) | | `destructive` | no (default) | errors, delete buttons, invalid states | red — same in light/dark | | `link` | no (default) | inline hyperlinks | blue — same in light/dark | | `success` | no (default) | success feedback (Toast/Progress success variant, confirmation states) | green — same in light/dark | | `warning` | no (default) | warning feedback (Toast warning, caution states) | amber — same in light/dark | | `info` | no (default) | informational feedback (Toast info, search-match highlights) | blue — same in light/dark | | `accent` | no (optional) | brand-vivid CTA, popular badge, focus rings, eyebrow dot | falls back to `primary` | ### `accent` fallback contract — REQUIRED Apps that don't define `accent` get `undefined` on those vars. **Every accent reference in CSS must chain a fallback:** ```css background-color: var(--v-accent-fill, var(--v-primary-fill)); color: var(--v-accent-foreground, var(--v-primary-foreground)); border-color: var(--v-accent-border, var(--v-primary-border)); ``` --- ## Motion Role-based duration scale + base easing/delay. Pick the tier that matches the interaction; don't tokenize one-offs. ``` --v-motion-instant 0ms (micro acks, debug) --v-motion-fast 150ms hover, focus ring --v-motion-default 250ms state transitions, accordion expand --v-motion-slow 400ms drawer slide, page reveal --v-motion-slower 600ms orchestrated/sequenced reveals --v-motion-duration 250ms alias of `default` (legacy) --v-motion-timing cubic-bezier(0.4, 0, 0.2, 1) --v-motion-delay 0ms ``` Sempre nomeie a propriedade no `transition` — nunca `transition: all`: ```css transition: background-color var(--v-motion-default) var(--v-motion-timing), color var(--v-motion-default) var(--v-motion-timing); ``` --- ## Page-level tokens Shipped by `@vireya/core` (defaults below) and used as the outer-gutter / max-width contract for blocks. Apps may override on `body` in their globals.css if they want a different rhythm. ``` --v-page-padding clamp(16px, 5vw, 32px) --v-page-max-width 1200px ``` Apps that want a wider canvas (e.g. `1520px`) override on `body`: ```css body { --v-page-max-width: 1520px; --v-page-padding: var(--v-width-16); } ``` --- ## Content-width tokens (block inner content) For inner-content max-widths (hero subtitle, section description, FAQ list, contact form). Page-level wrappers use `--v-page-max-width` instead. | Token | Value | Use | |---|---|---| | `--v-content-width-sm` | `560px` | Compact inner content (single contact form, narrow card) | | `--v-content-width-md` | `720px` | Standard section header (title + description), most CTA blocks | | `--v-content-width-lg` | `880px` | Wide hero copy, two-column splits, large quotes | --- ## Elevation Low-elevation system: borders + tone over shadows. Reach for the right tier: | Token | Value | Use | |---|---|---| | `--v-elevation-rest` | `none` | Resting cards — use border + tone, NOT shadow | | `--v-elevation-hover` | `0 8px 24px -12px rgba(0,0,0,.08)` | Hover lift on cards / tier cards | | `--v-elevation-popover` | `0 8px 24px -8px rgba(0,0,0,.12)` | Popovers, dropdowns, tooltips | | `--v-elevation-modal` | `0 24px 48px -16px rgba(0,0,0,.20)` | Modals, drawers, sheets | Forbidden: multi-layer shadows (`box-shadow: 0 1px 2px ..., 0 4px 8px ...`), warm tints (`rgba(20, 10, 0, .1)` — use pure black), resting shadows on non-floating surfaces. --- ## Common usage cheat sheet — "I want X, paste Y" A reverse-lookup index. Match your intent on the left → use the tokens on the right. **Every right-hand value is a complete CSS line, ready to paste.** ### Surfaces & containers | Intent | Tokens to use | |---|---| | Page background (default) | `background: var(--v-background-fill);` | | Subtle/muted background (sidebar, card-inside-card, table header) | `background: var(--v-secondary-fill);` | | Primary surface (sticky CTA strip, dark bg in light mode) | `background: var(--v-primary-fill); color: var(--v-primary-foreground);` | | Standard card / tile (resting) | `border: 1px solid var(--v-secondary-border); background: var(--v-background-fill); border-radius: var(--v-radius-xl);` | | Card hover lift | `transform: translateY(-2px); border-color: var(--v-secondary-active); box-shadow: var(--v-elevation-hover);` | | Floating layer (popover, dropdown) | `border: 1px solid var(--v-secondary-border); background: var(--v-background-fill); box-shadow: var(--v-elevation-popover); border-radius: var(--v-radius-lg);` | | Modal / drawer | `background: var(--v-background-fill); box-shadow: var(--v-elevation-modal); border-radius: var(--v-radius-2xl);` | ### Text | Intent | Tokens to use | |---|---| | Body default | `color: var(--v-background-foreground); font-size: var(--v-font-size-body); line-height: var(--v-leading-relaxed);` | | Muted body / description / caption | `color: var(--v-secondary-active);` *(NOT `--v-secondary-fill` — `-active` is the muted text on neutral)* | | Strong heading text | `color: var(--v-background-foreground); font-weight: var(--v-font-weight-semibold); line-height: var(--v-leading-snug);` | | Eyebrow / kicker (mono uppercase) | `font-family: var(--v-font-family-mono); font-size: var(--v-font-size-caption); text-transform: uppercase; letter-spacing: var(--v-tracking-eyebrow); color: var(--v-secondary-active);` | | Inline code / kbd | `font-family: var(--v-font-family-mono); font-size: var(--v-font-size-body-sm); padding: var(--v-height-2) var(--v-width-6); border-radius: var(--v-radius-sm); background: var(--v-secondary-fill); color: var(--v-background-foreground);` | | Link text (inline) | `color: var(--v-link-fill);` and `text-decoration-color: var(--v-link-border);` | | Destructive text (error helper, validation) | `color: var(--v-destructive-fill);` | ### Borders | Intent | Tokens to use | |---|---| | Default 1px border (most common) | `border: 1px solid var(--v-secondary-border);` | | Stronger border (focused/active card) | `border: 1px solid var(--v-secondary-active);` | | Soft divider line between sections | `border-top: 1px solid var(--v-secondary-border);` | | Dashed / decorative divider | `border-top: 1px dashed var(--v-secondary-border);` | | Destructive border (invalid input) | `border-color: var(--v-destructive-fill);` | ### Buttons / CTAs (when you can't use `