Skip to content
Component installs need the registry setup
VegaStack Design

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

KindExampleBehavior
Component (registry:ui)@vegastack/buttoncopied 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-searchplain .ts into components/ui/, same tracking
Icon item@vegastack/icon-arrow-rightone animated-icon mirror per item — 467 of them; bare names never collide with components
Block (registry:block)@vegastack/app-shell-01a 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-dataplain .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 chrome DataList and DataGrid share: 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 and onRowOpen'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 by EmojiPicker and ShortcutOverlay. 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 and TemplateEditor's @ / {{ picker. Its rows read exactly like ComboboxItem's (the accent highlight, an optional icon, muted trailing text or a keyboard hint), with optional group headings and a status for 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 through suggestionMenuKeyDown, and points aria-activedescendant at suggestionMenuOptionId(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 to min(60vh, 440px), the description a second line under the label); presentation="auto" opens a bottom Sheet with its own search field (query, onQueryChange, onClose) on a coarse pointer or a window under 640px, and useSuggestionMenuSheet(presentation) tells the editor when it does; an option's descriptionShown: "active" shows its description on the highlighted row only. It arrives with either editor.
  • @vegastack/person-avatar — PersonAvatar and the Person shape: the one way a person is drawn — their photo, else their initials (its personInitials rule) on their own hue, 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, SearchableSelect and AvatarPicker all compose it; its API is on the PersonHoverCard page.
  • @vegastack/table-scroll-region — the named, conditionally-focusable scroll viewport that ships inside Table'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 updates query at once and sends the request after TIMINGS.searchDebounceMs (300 ms) of quiet; a newer request aborts the last, and a late response is dropped; loadMore is a LoadMore state that fetches the next page with the returned cursor; a failure keeps the loaded items, sets error (from errorMessage, never the raw exception), and loadMore.onLoadMore retries it. enabled: false holds the first load (until a popup opens, say); deps reset 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 bare shadcn add dialog installs shadcn's own Radix-based dialog — a different component tree that will not match this system (and overwrites files if names collide).
  • registryDependencies come transitively: adding @vegastack/field also brings input; data-list brings table+checkbox+skeleton+empty; provider brings toast+tooltip; the modal families bring the internal use-modal-inert focus-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.

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, --radius or a font family once and every component follows (Theming).
  • className at the call site — every part accepts one and merges it with cn().
  • A wrapper in your own file — wrap Button in your own SubmitButton rather than forking button.tsx for 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 status update or drift, 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.

On this page