npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@archetypeai/ds-ui-svelte-console

v0.17.0

Published

Archetype AI Design System Console Primitives (Svelte 5 + ds-lib-tokens Integration)

Readme

@archetypeai/ds-ui-svelte-console

Console primitives of the Archetype AI design system. Svelte 5 primitives built on bits-ui, styled with Tailwind v4 and tailwind-variants, and wired to the @archetypeai/ds-lib-tokens theme. TypeScript source ships as .svelte files (no precompilation) so consumers get full type-checking and IDE go-to-definition.

The console flavor ships two ways from the same source, with different roles:

  • npm package (this document's default) - how apps consume primitives: they stay in node_modules, imported per-subpath, updated by version bump. The package is the source of truth.
  • shadcn-svelte registry - how primitives get modified or extended: pull a primitive's editable source into your app, change it, then port the change back into the package. See Modifying a primitive (registry workflow).

Install

npm i @archetypeai/ds-ui-svelte-console

Then install all peer dependencies:

npm i svelte tailwindcss bits-ui tailwind-variants tailwind-merge clsx @lucide/svelte @archetypeai/ds-lib-tokens svelte-sonner shiki yaml @cfworker/json-schema

Tailwind v4 setup (mandatory)

Tailwind v4 does not scan node_modules by default. Without the @source directive below, every class in this package is purged at consumer build time and components render unstyled - this is the single most common silent failure.

In your consumer's app.css, in this order:

@import 'tailwindcss';
@import '@archetypeai/ds-lib-tokens/theme.css';
@source "../node_modules/@archetypeai/ds-ui-svelte-console/dist";

Order matters:

  • tailwindcss registers the engine.
  • @archetypeai/ds-lib-tokens/theme.css declares the CSS variables that component classes consume (atai-neutral, bg-card, stroke-icon-default, etc.).
  • @source makes Tailwind scan this package's .svelte files so utility classes used inside the package are emitted into your consumer bundle.

Adjust the @source path if your app.css lives elsewhere - it must resolve to the package's dist/ directory.

TypeScript setup

In your consumer's tsconfig.json (or jsconfig.json), set:

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

"node16" and "nodenext" also work. Without one of these, the types condition in this package's exports map is bypassed, types fall back to the default JS file, and editor features degrade.

Usage

Each primitive lives at its own subpath. There is no barrel import { Button } from '@archetypeai/ds-ui-svelte-console' - subpaths keep svelte-sonner, shiki, yaml, and @cfworker/json-schema out of bundles for consumers who never touch the codeblock or sonner primitives.

<script lang="ts">
  import { Button } from '@archetypeai/ds-ui-svelte-console/primitives/button'
  import { Badge } from '@archetypeai/ds-ui-svelte-console/primitives/badge'

  let buttonRef: HTMLButtonElement | null = $state(null)
</script>

<Badge variant="status">Hello</Badge>
<Badge variant="status" size="sm">Tight</Badge>
<Button bind:ref={buttonRef} variant="primary" size="md">Click me</Button>

Per-primitive subpaths

| Primitive | Import path | |---|---| | Alert | @archetypeai/ds-ui-svelte-console/primitives/alert | | Avatar | @archetypeai/ds-ui-svelte-console/primitives/avatar | | Badge | @archetypeai/ds-ui-svelte-console/primitives/badge | | Button | @archetypeai/ds-ui-svelte-console/primitives/button | | Card | @archetypeai/ds-ui-svelte-console/primitives/card | | Checkbox | @archetypeai/ds-ui-svelte-console/primitives/checkbox | | CodeBlock | @archetypeai/ds-ui-svelte-console/primitives/codeblock | | Collapsible | @archetypeai/ds-ui-svelte-console/primitives/collapsible | | CopyButton | @archetypeai/ds-ui-svelte-console/primitives/copy-button | | Counter | @archetypeai/ds-ui-svelte-console/primitives/counter | | Dialog | @archetypeai/ds-ui-svelte-console/primitives/dialog | | DropdownMenu | @archetypeai/ds-ui-svelte-console/primitives/dropdown-menu | | DropZone | @archetypeai/ds-ui-svelte-console/primitives/dropzone | | EmptyState | @archetypeai/ds-ui-svelte-console/primitives/empty-state | | Input | @archetypeai/ds-ui-svelte-console/primitives/input | | InputGroup | @archetypeai/ds-ui-svelte-console/primitives/input-group | | Item | @archetypeai/ds-ui-svelte-console/primitives/item | | Label | @archetypeai/ds-ui-svelte-console/primitives/label | | Progress | @archetypeai/ds-ui-svelte-console/primitives/progress | | Select | @archetypeai/ds-ui-svelte-console/primitives/select | | Separator | @archetypeai/ds-ui-svelte-console/primitives/separator | | SidePanel | @archetypeai/ds-ui-svelte-console/primitives/side-panel | | Sonner | @archetypeai/ds-ui-svelte-console/primitives/sonner | | Spinner | @archetypeai/ds-ui-svelte-console/primitives/spinner | | Table | @archetypeai/ds-ui-svelte-console/primitives/table | | Tabs | @archetypeai/ds-ui-svelte-console/primitives/tabs | | Textarea | @archetypeai/ds-ui-svelte-console/primitives/textarea | | Tooltip | @archetypeai/ds-ui-svelte-console/primitives/tooltip |

The cn() helper and shared TS type helpers (WithElementRef, WithoutChild, WithoutChildren, WithoutChildrenOrChild) are exported from @archetypeai/ds-ui-svelte-console/primitives/utils. The dark-mode controller lives at @archetypeai/ds-ui-svelte-console/primitives/theme (see below).

Composition patterns will arrive at @archetypeai/ds-ui-svelte-console/patterns/<name> in a later release without breaking any of the primitive paths above.

Dark mode (primitives/theme)

The package manages dark mode without any external dependency. darkMode is a reactive singleton that mirrors the dark class on <html>:

<script lang="ts">
  import { darkMode } from '@archetypeai/ds-ui-svelte-console/primitives/theme'
</script>

<button onclick={() => darkMode.toggle()}>
  {darkMode.current ? 'Switch to light' : 'Switch to dark'}
</button>
  • darkMode.current - reactive boolean, true while <html> carries the dark class. Safe to read in $derived/$effect.
  • darkMode.set(value) - adds/removes the class. CSS transitions are suppressed for one frame during the swap, so the whole page flips at once instead of staggering per-element.
  • darkMode.toggle() - convenience for set(!current).

A MutationObserver keeps current in sync even when something else toggles the class (e.g. an app-level theme store or SSR-rendered markup), and the module is SSR-safe - it no-ops without a document. Primitives themselves don't consume darkMode — they theme off the class through CSS tokens (CodeBlock's syntax highlighting renders both themes up front and switches between them in CSS).

Avatar (primitives/avatar)

Root plus a fallback is the minimum - Avatar.Root alone renders an empty circle:

<script lang="ts">
  import * as Avatar from '@archetypeai/ds-ui-svelte-console/primitives/avatar';
</script>

<Avatar.Root size="sm">
  <Avatar.Image src={user.avatarUrl} alt={user.name} />
  <Avatar.Fallback>{initials}</Avatar.Fallback>
</Avatar.Root>

The fallback covers three cases, not one: the image is still loading, there is no src, and the image failed - so initials belong there even when you expect a photo. Avatar.Image is a real <img>, so alt is the caller's to pass; use the person's name.

size is sm (28px) / default (36px) / lg (48px) and shape is circle / squircle - circle for people, squircle for entities like an org or a workspace. The fallback's type scales off the root's data-size, so resize with the variant rather than a hand-written size-* override, or the two desynchronise. Pass loadingStatus="loaded" to skip the fallback flash for an already-cached image; it is bindable if you need to read the outcome.

To stack a group, put flex -space-x-2 plus *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:border-0 on the container - the ring cuts each avatar out of the one behind it, and hangs off data-slot rather than needing a prop. Drop the border in a stack: the root's border-border sits just inside the ring, so keeping it renders a grey rim, then a white gap, then the next rim, and the overlap reads muddy. Stack images, not initials: the overlap covers each avatar's trailing edge, which a face survives and two letters do not.

CopyButton (primitives/copy-button)

A copy control that owns its own copied state, so a table of them needs no bookkeeping in the caller:

<script lang="ts">
  import { CopyButton } from '@archetypeai/ds-ui-svelte-console/primitives/copy-button';
</script>

<CopyButton value={job.id} label="Copy job ID" title={job.id} />

label is the accessible name - say what is being copied. Pass the value as children instead and the whole control becomes one copy target; the hit area follows automatically (the 36px icon size when icon-only, inline when the value is inside).

It toasts by default, so a <Toaster /> must be mounted (see primitives/sonner) or the toast silently no-ops. Pass toast={false} where the inline check mark is feedback enough - a table of IDs, where a toast per copy is noise - and the control announces the copy to screen readers itself instead. successMessage / errorMessage change the wording; onCopy(copied) hooks anything else.

copyText(text) is exported from the same subpath for a labelled action that happens to copy, without the button.

CodeBlock editor & validation

primitives/codeblock exports two components: CodeBlock (read-only, shiki-highlighted) and CodeBlockEditor (editable, with a YAML + JSON Schema validation pipeline):

<script lang="ts">
  import { CodeBlockEditor } from '@archetypeai/ds-ui-svelte-console/primitives/codeblock'

  let value = $state('name: my-config')
  let valid = $state(true)
</script>

<CodeBlockEditor bind:value bind:valid schema={mySchema} showDownload downloadBaseName="config" />
  • value (bindable) - the YAML text being edited.
  • schema - optional JSON Schema object; when provided, the parsed YAML is validated with @cfworker/json-schema.
  • valid (bindable) - true while the text parses and passes the schema.
  • schemaUnavailable (bindable) - true when the schema itself can't be compiled into a validator.
  • schemaFallbackFix - overrides the fix line shown when the schema fails without a specific field to point at (the generic default is "Check each setting against the expected format.").
  • placeholder, showDownload, downloadBaseName - editor chrome.

Validation is debounced (250 ms) and errors are mapped back to source lines as what-happened + fix pairs: schema pointers render as model.learning_rate (not /model/learning_rate), schema types as plain words ("a number", "a whole number", "text"), and the common schema keywords (type, required, unrecognized settings, enum/const, min/max) get dedicated wording. The shiki highlighter is lazy-loaded, and highlights against github-light and github-dark in a single pass — the dark colours ride --shiki-dark custom props that CSS swaps in under .dark, so a theme flip costs no re-highlight. This pipeline is why yaml and @cfworker/json-schema are peer dependencies.

Tooltip (primitives/tooltip)

Tooltip.Trigger variant="info" is the field-help affordance, and it renders itself - a filled 16px disc holding a mono i. Pass it no children:

<script lang="ts">
  import * as Tooltip from '@archetypeai/ds-ui-svelte-console/primitives/tooltip';
</script>

<div class="flex items-center gap-2">
  <Label variant="sectionHeader">Job Name</Label>
  <Tooltip.Root>
    <Tooltip.Trigger variant="info" aria-label="About Job Name" />
    <Tooltip.Content>A human-readable label for this batch job.</Tooltip.Content>
  </Tooltip.Root>
</div>

aria-label is required and is yours to pass - name what is being explained. The glyph is aria-hidden, so a trigger without a label has no accessible name at all rather than one called "i". An icon child still renders (scaled down to fit), but it is no longer the recipe: the disc is already the affordance, and an icon inside it reads as two.

The disc is size-4, which squares against Counter size="sm"'s h-4 on purpose - a section title, its Counter and its info disc in one flex items-center gap-2 row is the intended cluster, the solid pill reading as the count and the muted one as the meaning. Both stay filled; a stroked ring beside a filled pill reads as a half-drawn one.

16px is also exactly a Label variant="sectionHeader"'s box height, so the disc sits level with the title instead of towering over it. That label is text-xs, and every step of the type scale carries its own line-height, so text-xs alone makes it a 16px box. workbenchHeader (text-sm, a 20px line box) is the variant size="lg" lines up with.

size="lg" is the 20px disc (size-5, glyph at text-xs), for the rarer cluster built around Counter size="default" rather than a section label. size only bites on the info variant - the other trigger treatments have no box for it to change - and it is a compound axis, so size on inlineLink is inert rather than wrong.

Both sizes set the glyph's type size on the glyph ([&>span]:text-[10px]), not just on the button. That looks redundant but is not: ds-lib-tokens' base layer carries span { @apply text-sm }, and the info glyph is a span, so a glyph left to inherit renders at 14px no matter what the button says. Editing the disc's type size means editing the [&>span]: half too, or the glyph silently stops tracking the disc - tooltip.test.ts pins it against Counter size="sm" to catch that.

variant="inlineLink" is the other trigger treatment - an underlined span flowing inside prose, for a term that needs a definition without a disc interrupting the line.

Tooltip.Content takes arrowClasses for the arrow's own styling, but not for placement: the arrow centres itself on the trigger on all four sides and protrudes half its box. A tip that looks off-centre is a bug in tooltipArrowVariants to fix here, not something to counter-shift at the call site - a call-site nudge is invisible to every other consumer and silently double-corrects the day the primitive is fixed.

API conventions

  • bind:ref for element handles. Every primitive exposes ref as a $bindable prop. Use <Button bind:ref={el} /> to capture the underlying DOM element.

  • data-* attributes are public API. Every variant slot carries data-slot plus variant attributes (data-variant, data-size, data-state, data-typography, etc.). Use them as descendant-selector hooks. Renaming requires a major version bump.

  • Composite primitives compose by sub-part. Card, Dialog, Item, Select, DropdownMenu, Tabs, Collapsible, Tooltip, InputGroup, and DropZone export their sub-parts (e.g. Dialog.Trigger, Dialog.Content). Pass class to each sub-part directly - there is no headerClass/contentClass shorthand on the root.

  • child snippet on Item is the bits-ui asChild render-prop pattern, not a typo for children.

  • Counts are their own primitive, and absent is not zero. Counter (and Tabs.Count, the tab-context wrapper around it) takes a value: a number renders the pill, null renders the not-yet-loaded pill at the same reserved width, and omitting the component entirely is how a tab says it has no count. Values past 999 abbreviate (1.2K, 123K) so a growing count can never widen the strip; the exact number stays on title and in the accessible name. A caller that knows only "at least this many" (a keyset-paginated list, a capped query) passes atLeast alongside the value to render it as a floor (103+, accessible name "at least 103"); the abbreviation truncates rather than rounds in that mode, so the floor never claims more than was loaded. The pill is the default, not the whole component: variant="bare" size="inherit" drops the background, the radius and the mono face and takes the typography of whatever it sits in, so a headline figure, a table cell or a number mid-sentence can roll on the same odometer. Bare counters sit on the surrounding baseline and reserve no width of their own; exact turns off the abbreviation where the reader is meant to read the figure rather than glance at it.

  • A sortable column header never holds the sort. Table.Head sortable turns the header into a button and owns the affordance, the three-state indicator and aria-sort; the consumer passes direction ('asc' | 'desc' | null) and an onSort callback and keeps the state itself. null means another column holds the sort, which is what renders that header's unsorted arrow. Because the state lives with the consumer, a Sort menu and the column headers can drive the same state and stay in step. Heads without sortable are unchanged and are simply not clickable.

  • Variant configs are exported. Each index.ts re-exports its tv() configs (e.g. buttonVariants, inputGroupButtonVariants) so consumers can compose variants without forking source.

Modifying a primitive (registry workflow)

The npm package is the source of truth for primitives - consume every primitive from it, and don't keep local forks or hand-roll one-offs. What you will hit is a primitive that needs to change: an internal refactor, a variant's styling adjusted, or the API extended with a new variant or prop axis. For that, the same primitives are served as a shadcn-svelte registry at https://design-system-console.archetypeai.workers.dev/, so you can work on the real source inside your app and upstream the result:

  1. Pull the editable source into your app:

    npx shadcn-svelte@latest add https://design-system-console.archetypeai.workers.dev/r/button.json

    Components install flat under your ui alias - $lib/components/ui/{button,codeblock,...}/, plus utils.ts and theme.svelte.ts at the ui root. Cross-component dependencies are declared with the registry's local: prefix, so installing one component pulls everything it needs from the same registry - e.g. codeblock pulls button, spinner, theme, and console-utils.

  2. Make the change against the local source and validate it in-app.

  3. Port the change back into ds-ui-svelte/console/primitives/ in the design-system repo and release. Then bump the package version in your app, delete the local copy, and import from primitives/<name> again. A copy living long-term in ui/ is drift - the package, not the app, owns primitives.

A genuinely new primitive follows the same path in reverse: build it in ui/ mirroring the existing primitives (a tailwind-variants config, cn from utils, data-slot/data-variant attributes, runes, token classes throughout - read an installed primitive's source as the template), then ship it into the package.

Notes:

  • Always install via the full registry URL (or a project script wired to it - the console app exposes npm run ds [<name>] for exactly this). A bare npx shadcn-svelte@latest add button resolves against the default shadcn-svelte registry and installs the generic upstream component, not the Archetype one.
  • The utils item is named console-utils (shadcn-svelte reserves utils); it ships the console cn() with the rounded-interactive tailwind-merge extension. Without it, class merging on interactive radii silently breaks.
  • /r/all.json lists all component URLs for programmatic access.
  • Registry-installed files don't need the Tailwind @source directive (they live in your repo and are scanned normally), but the @archetypeai/ds-lib-tokens/theme.css import remains mandatory.

Extending in labs

Console is the stable base tier: its API contract is frozen and its data-* attributes are public API that only a major version may change. When you need new or experimental behavior on top of a console primitive, build it in the labs tier (@archetypeai/ds-ui-svelte-labs) - never fork or mutate console to accommodate a one-off.

Labs adapts console without changing it, using an escape-hatch ladder - reach for the lowest rung that solves the problem:

  1. class prop override. Every primitive forwards class to its root slot; pass Tailwind utilities to restyle in place.
  2. data-slot / data-* selectors in your CSS. Target the public attributes ([data-slot="..."], [data-variant="..."], data-state) with descendant selectors - no source changes.
  3. tv({ extend }) on the exported variant cluster. Each primitive re-exports its tailwind-variants config (e.g. buttonVariants); extend it to add a variant or override defaults without forking the component.
  4. A labs wrapper component (rare). When the first three can't express it, compose the console primitive inside a labs primitive.

This ladder keeps the modification surface inside labs. Behavior that proves itself there graduates back into console in a deliberate, API-hardening change - the flow is always labs → console, never console reaching up into labs. See the labs README for the consumer-side view of the same ladder.

License

MIT. See LICENSE.