@verta/ui (0.3.0)
Installation
@verta:registry=npm install @verta/ui@0.3.0"@verta/ui": "0.3.0"About this package
@verta/ui
The shared Verta component library — shadcn-style components built on Radix
primitives + CVA variants + cn (clsx + tailwind-merge), themed entirely
from @verta/tokens. One library instead of every repo keeping its
own src/components/ui.
Components
Button (primary/secondary/ghost/danger/accent · md/sm/icon), Card, Table,
Input / Textarea / Label / Hint / Field, Select, Dialog, Tabs, Badge / Pill,
StatusDot, Toast, AppShell (frosted sticky header + nav), ThemeToggle. Icons via
lucide-react.
Component contracts
Conventions that bit consumers during the identity-admin rollout — pinned here so recall / iskra don't repeat them.
Card padding
The root <Card> is padding-free by design; padding lives in the slots
CardHeader / CardContent / CardFooter (each p-5). Two layouts, never mixed:
- Slotted (default):
<Card><CardHeader/><CardContent/><CardFooter/></Card>— the slots own the padding. - Padded block:
<Card padded>{content}</Card>— the root getsp-6for a single self-contained block with no slots.
A bare <Card>{content}</Card> (no slot, no padded) renders flush to the border —
add padded. Don't combine them (a padded root wrapping CardContent double-pads).
padded is opt-in (default false), so existing slotted cards are unchanged.
Button semantics: primary vs accent
Button defaults to primary = bg-fg — a neutral fill (near-black in dark,
near-white in light). The colored call-to-action is variant="accent"
(bg-accent, #3291ff). This is the opposite of some pre-canon local kits where
primary was the blue accent — porting those 1:1 makes every primary CTA render
neutral/black (identity saw ~37 CTAs "go black": Save / Generate / Create…).
- Migration: a CTA that should read as the colored primary action must be
<Button variant="accent">, not the default. Audit each ported<Button>/variant="primary"and switch the genuine accent CTAs toaccent. - Decision: no accent-default in the kit —
primarystays neutral (canon: neutral primary, one accent CTA per view). Consumers opt intoaccentper CTA. (kit-fix: none — consumer-side migration + this doc.)
Sizing & spacing parity (kit vs pre-canon local)
Reconciliation of kit metrics against the pre-canon local kits, decision per row:
| Element | Kit (canon) | Pre-canon local | Decision |
|---|---|---|---|
Input / SelectTrigger height |
h-[38px] |
36px | Keep 38px (canon control height). Consumer-side: align hand-rolled <select>/<input> to 38px, or wrap in the kit components. |
Button height (md) |
h-9 (36px) |
36px | Keep 36px. Action buttons and text controls intentionally differ by 2px — documented, not a bug. |
Badge / Pill x-padding |
px-2.5 |
px-2 | Keep px-2.5 (canon); cosmetic, consumer-side. |
Sheet width / padding |
w-72/96/[32rem], p-6 |
varied | Keep canon. Use the size prop; custom widths via className. |
AppShellSidebar padding |
p-3, item px-2.5 py-2 |
varied | Keep canon; consumer-side. |
Tailwind v4 defaults to verify in consumers:
border-color— v4's bareborderutility resolves tocurrentColor(v3 was gray-200). The kit always sets an explicit colour (border-border), so kit components are safe; consumers using a barebordershould add a base reset (*, ::after, ::before { border-color: var(--color-border); }) or always pairborderwithborder-border.ring— the kit uses explicitring-2 ring-accent/55(+ thering-focus-ringtoken), so it does not rely on v4's 1pxcurrentColorring default.- space-scale — the kit uses Tailwind's standard numeric scale; the token spacing
scale is consumed via arbitrary values (
p-[var(--space-4)]) so it never shadows it.
Install (package model)
pnpm add @verta/ui # pulls @verta/tokens; requires the @verta registry in .npmrc
Consumer entry CSS (Tailwind v4):
@import "@verta/ui/styles.css"; /* = @verta/tokens fonts + tokens + theme */
@import "tailwindcss";
@source "../node_modules/@verta/ui/dist"; /* scan component classes */
import { Button, Card, ThemeToggle } from "@verta/ui";
Registry model (own the source)
Components are also emitted as a shadcn-style registry under dist/registry/
(@verta/ui/registry), so you can copy a component's source into your repo and
own it instead of importing the package.
Develop
pnpm --filter @verta/ui build # tsup (ESM + d.ts) + styles + registry
pnpm --filter @verta/ui test # Vitest + Testing Library (jsdom)
pnpm --filter @verta/ui typecheck
Gallery (Ladle)
The live component catalogue runs on Ladle — a Vite-native
Storybook alternative. Stories are co-located next to each component
(src/components/<name>.stories.tsx); the whole gallery is wired in .ladle/.
pnpm --filter @verta/ui ladle # dev server → http://localhost:61000
pnpm --filter @verta/ui ladle:build # static build → ladle-dist/ (deployable to nginx/preview)
The gallery reads @verta/tokens straight from workspace source (aliased in
.ladle/vite.config.ts), so it runs on a clean checkout with no prior build.
The toolbar theme toggle (bottom-left) drives the .light class via
@verta/tokens — every component is shown in both dark and light. Key props
(variant, size, disabled, tone, status) are exposed as interactive
controls on each component's Playground story.
Add a story for a new component
Drop a *.stories.tsx next to the component — no other wiring needed:
// src/components/widget.stories.tsx
import type { Story, StoryDefault } from "@ladle/react";
import { Widget, type WidgetProps } from "./widget";
export default { title: "Components / Widget" } satisfies StoryDefault;
export const Playground: Story<{ variant: WidgetProps["variant"] }> = ({ variant }) => (
<Widget variant={variant} />
);
Playground.args = { variant: "primary" };
Playground.argTypes = {
variant: { options: ["primary", "secondary"], control: { type: "select" } },
};
Ladle does not infer controls from TS types — declare them via args /
argTypes. For components that portal into document.body (Dialog, Select,
Toast), add meta = { iframed: true } so the overlay doesn't cover the Ladle
navigation. *.stories.tsx files are excluded from the published bundle and the
shadcn registry.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @radix-ui/react-avatar | ^1.1.11 |
| @radix-ui/react-checkbox | ^1.3.3 |
| @radix-ui/react-dialog | ^1.1.15 |
| @radix-ui/react-dropdown-menu | ^2.1.16 |
| @radix-ui/react-label | ^2.1.8 |
| @radix-ui/react-popover | ^1.1.15 |
| @radix-ui/react-select | ^2.2.6 |
| @radix-ui/react-slot | ^1.2.4 |
| @radix-ui/react-switch | ^1.2.6 |
| @radix-ui/react-tabs | ^1.1.13 |
| @radix-ui/react-toast | ^1.2.15 |
| @radix-ui/react-tooltip | ^1.2.8 |
| @verta/tokens | 0.3.0 |
| class-variance-authority | ^0.7.1 |
| clsx | ^2.1.1 |
| echarts | ^6.1.0 |
| lucide-react | ^1.17.0 |
| tailwind-merge | ^3.6.0 |
Development dependencies
| ID | Version |
|---|---|
| @ladle/react | ^5.1.1 |
| @tailwindcss/vite | ^4.1.13 |
| @testing-library/jest-dom | ^6.9.1 |
| @testing-library/react | ^16.3.2 |
| @testing-library/user-event | ^14.6.1 |
| @types/react | ^19.2.15 |
| @types/react-dom | ^19.2.3 |
| jsdom | ^29.1.1 |
| react | ^19.2.6 |
| react-dom | ^19.2.6 |
| tailwindcss | ^4.1.13 |
| tsup | ^8.5.1 |
| typescript | ^5.6.2 |
| vite | ^6.3.6 |
| vitest | ^2.1.3 |
Peer dependencies
| ID | Version |
|---|---|
| react | ^18 || ^19 |
| react-dom | ^18 || ^19 |