Working with components
The copy-in ownership model end to end — add, verify, customize, and pull updates tracked by content.
Last updated
VegaStack components are copy-in: shadcn add writes real source files into
your repo and you own them. Nothing updates behind your back; updates are pulled
when you decide. This page is the full lifecycle.
The five item kinds
| Kind | Example | Behavior |
|---|---|---|
Component (registry:ui) | @vegastack/button | copied to components/ui/, update-tracked by content |
Hook (registry:hook) | @vegastack/use-media-query, @vegastack/use-mobile, @vegastack/use-list-nav, @vegastack/use-platform, @vegastack/use-drag-reorder, @vegastack/use-file-drop, @vegastack/use-animation-replay, @vegastack/use-modal-inert, @vegastack/use-async-search | plain .ts into components/ui/, same tracking |
| Icon item | @vegastack/icon-arrow-right | one animated-icon mirror per item — 467 of them; bare names never collide with components |
Block (registry:block) | @vegastack/app-shell-01 | a multi-file starter composition (a page and its components) you own outright — deliberately NOT update-tracked |
Lib (registry:lib) | @vegastack/geo-data, @vegastack/drag-item, @vegastack/tile-overlay, @vegastack/disclosure-trigger, @vegastack/page-layout, @vegastack/emoji-data | plain .ts into your lib/ alias — data and class-string recipes with no React in them, pulled in automatically as a dependency of the components that read them, and installed once however many of those you add |
Shared internals
A few items are components other components install, not components you pick. They have no page of their own because there is nothing to choose: you get them by adding the component that needs them.
@vegastack/data-table-parts— the table chromeDataListandDataGridshare: the sort header, the select-all head and selection cell, skeleton rows, the empty row, the column class rules, the docked selection bar (SelectionActionBar), the open-row wash andonRowOpen's details, the one list density scale (LIST_ROW_HEIGHT_*), and the selection / sort / controlled-state hooks. It arrives with either component, and installing both installs it once. Its parts are documented on those two pages, where they are actually used.@vegastack/panel-search— the sticky, box-free search row a filtering popup puts at the top of its panel, shared byEmojiPickerandShortcutOverlay. A bordered field inside a bordered popup nests two borders, so the ROW draws the one hairline and tints it on focus while the field paints no box at all. It arrives with either popup, and installing both installs it once.@vegastack/suggestion-menu— the caret-anchored list an editor opens while you type a trigger:TextEdit's slash and mention menus andTemplateEditor's@/{{picker. Its rows read exactly likeComboboxItem's (theaccenthighlight, an optional icon, muted trailing text or a keyboard hint), with optional group headings and astatusfor empty, loading and error. It portals to<body>, flips above the caret when below is short, and never takes focus: the editor keeps the caret, owns the keys throughsuggestionMenuKeyDown, and pointsaria-activedescendantatsuggestionMenuOptionId(listId, index). The pointer moves the same active row by MOVING over it, so a keyboard scroll under a resting pointer never steals it.size="lg"is the roomy block menu (320px, up tomin(60vh, 440px), the description a second line under the label);presentation="auto"opens a bottomSheetwith its own search field (query,onQueryChange,onClose) on a coarse pointer or a window under 640px, anduseSuggestionMenuSheet(presentation)tells the editor when it does; an option'sdescriptionShown: "active"shows its description on the highlighted row only. It arrives with either editor.@vegastack/person-avatar—PersonAvatarand thePersonshape: the one way a person is drawn — their photo, else their initials (itspersonInitialsrule) on their ownhue, else on the muted fallback. An agent (kind: "agent") is a bot on the purple hue, and keeps a purple bot badge at the corner when it has a photo, so it never passes for a person.PersonHoverCard,RecordChip,BoardCard,Inbox,Comments,SearchableSelectandAvatarPickerall compose it; its API is on the PersonHoverCard page.@vegastack/table-scroll-region— the named, conditionally-focusable scroll viewport that ships insideTable's item.
Hooks
A hook renders nothing, so it has no page of its own; its contract is its JSDoc and the pages of the components built on it.
@vegastack/use-async-search—useAsyncSearch(load, options)is the server-search state machine for a searched, cursor-paged list.load(query, { cursor, signal })returns{ items, nextCursor }; the hook returns{ items, query, onSearchChange, loading, error, loadMore, reload }. Typing updatesqueryat once and sends the request afterTIMINGS.searchDebounceMs(300 ms) of quiet; a newer request aborts the last, and a late response is dropped;loadMoreis aLoadMorestate that fetches the next page with the returned cursor; a failure keeps the loaded items, setserror(fromerrorMessage, never the raw exception), andloadMore.onLoadMoreretries it.enabled: falseholds the first load (until a popup opens, say);depsreset the results when a scope changes.
const customers = useAsyncSearch((query, { cursor, signal }) =>
searchCustomers({ query, cursor, signal }),
);
<DataList
columns={columns}
data={customers.items}
loadMore={customers.loadMore}
toolbar={
<SearchInput
aria-label="Search customers"
value={customers.query}
onValueChange={customers.onSearchChange}
/>
}
/>;Adding
pnpm dlx shadcn@latest add @vegastack/dialog- Always the
@vegastack/prefix. A bareshadcn add dialoginstalls shadcn's own Radix-based dialog — a different component tree that will not match this system (and overwrites files if names collide). registryDependenciescome transitively: adding@vegastack/fieldalso bringsinput;data-listbringstable+checkbox+skeleton+empty;providerbringstoast+tooltip; the modal families bring the internaluse-modal-inertfocus-containment hook. You never chase dependencies manually or call that modal lifecycle hook yourself.- npm dependencies of each item (
@base-ui/react,class-variance-authority, …) are installed by the CLI into your project automatically.
Every registry item carries a provenance header in its source:
// @vegastack dialog@0.1.0 sha256-Qm3…=Heads-up: the current shadcn CLI strips leading comments during copy-in, so
the header usually won't appear in your copied file — that's fine. Update
tracking does not depend on it: check-updates identifies your copies by
filename against the registry index and compares alias-normalized content.
If a header is present (older CLIs, or files synced by other tooling), it's
used as a fast-path version pin — keep it in that case.
Verifying integrity (recommended for CI-critical repos)
The registry signs its item manifest (Sigstore, GitHub OIDC identity) and every
item carries a whole-item SHA-256. The vegastack-design CLI (a bin inside
@vegastack/design — already installed) closes the fetch-time/copy-time gap:
# 1. verify the item BEFORE copy-in, save the verified bytes
VEGASTACK_VERIFY_DIR="$(mktemp -d "${TMPDIR:-/tmp}/vegastack-verify.XXXXXX")"
VEGASTACK_ITEM="$VEGASTACK_VERIFY_DIR/dialog.json"
pnpm exec vegastack-design verify --save "$VEGASTACK_ITEM" dialog
VEGASTACK_EXPECTED_INTEGRITY="$(node -e \
'process.stdout.write(JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).meta.integrity)' \
"$VEGASTACK_ITEM")"
# 2. copy in
pnpm dlx shadcn@latest add @vegastack/dialog
# 3. prove the files on disk match the verified bytes (alias-rewrite aware)
pnpm exec vegastack-design verify --post-write --item "$VEGASTACK_ITEM" \
--expected-integrity "$VEGASTACK_EXPECTED_INTEGRITY" --target-dir .The save path must not already exist. The private directory prevents pre-creation or symlink redirection; retaining the digest before copy-in also detects replacement of the saved item.
--hash-only skips the Sigstore signature check for local development when
cosign isn't installed; full mode verifies the signed manifest identity too.
Customizing
Don't edit a copied-in component; customize around it. In order of preference:
- A token override — retune
--primary,--radiusor a font family once and every component follows (Theming). classNameat the call site — every part accepts one and merges it withcn().- A wrapper in your own file — wrap
Buttonin your ownSubmitButtonrather than forkingbutton.tsxfor one use case.
If you must edit one, it becomes yours. From then on check-updates reports it as
≈ differs from registry — the content comparison can't tell your edit from an upstream change,
so every update becomes a --diff you review and re-apply by hand. Keep such edits additive
(add a variant or a class; don't reshuffle the file) so that diff stays readable.
CI note: --fail-on-update treats that status as a failure too, so an edited file keeps the
drift gate red until you scope it out: check-updates --fail-on-update --filter <unedited,...>.
Updating — pulled, never pushed
VegaStack-owned applications have an unlisted internal guide to how registry pulls relate to npm bumps, including the two-speed propagation model and the token-ordering trap.
pnpm exec vegastack-design check-updates ⬆ button 0.1.0 → 0.2.0 update available
≈ card → 0.2.0 differs from registry (update or local edits — review with --diff)
✓ field 0.2.0 up to date
? legacy 0.1.0 not in registry (renamed/removed)Status is by content, not version numbers: headered files compare their
integrity hash; headerless files (the normal real-CLI case) compare
alias-normalized bytes against the live item. Either way a component reads
up to date when a newer design-system release did not touch that component —
each item's version is the release in which its content last changed, so you only
re-pull what actually moved. The ≈ differs
status means the bytes diverged — an upstream update or your local edits;
add --diff shows which.
Per component, review then take:
pnpm exec shadcn add @vegastack/button --diff # see exactly what changed
pnpm exec shadcn add @vegastack/button --overwrite # take it--overwrite replaces the file — if you customized it, git diff afterwards and
re-apply your edits (they're in your git history; nothing is lost).
Upgrading from 0.4.x is the one time every component reads as an update at once, because every file changed. Do it in the order in Migrating to the shadcn reset — re-pulling before the call sites compile makes the errors unreadable.
Useful flags: --dir (scan a non-default location), --filter <name>, --json
(machine-readable — the right entry point for agents), --fail-on-update (exit
non-zero when anything is stale — the CI drift gate), --registry / --cwd.
Credentialed requests are pinned to VEGASTACK_TRUSTED_REGISTRY_ORIGIN (production by
default) and reject redirects; checkout-local dotenv files cannot redefine that trust anchor.
For agents: the reliable loop is
vegastack-design check-updates --json→ for each item with statusupdateordrift,shadcn add @vegastack/<name> --diff→ present the diff → only after approval--overwrite. Never hand-edit a provenance header, and never "update" by pasting source from elsewhere — pull through the CLI so content and registry stay comparable.
Blocks are different — by design
app-shell-01 copies once and is then yours; check-updates will not track
its page files. Rationale: a starter composition you've built a real page on
top of must never be at risk of an upstream overwrite. Its underlying
components (Card, Chart, Sidebar, …) remain individually tracked like any
other.
Shared identity picker internals
picker-panel owns search, recent rows, category navigation and cells for IconPicker and EmojiPicker. picker-search and picker-preferences provide ranked matching and optional local convenience state. icon-data is the 160-entry server-safe catalogue; icon-components contains explicit Lucide imports for IconGlyph. These dependencies install with their consumers.
Agent skills
Install the VegaStack agent skills so Claude Code and Codex build on-system by default — what each one covers, and how to install and update them.
Building a multi-step flow
Which component to reach for, how guards and branching work, and when to drop to Stepper and drive the flow yourself.