Skip to content

Releases

Changelog

One post per release of @codeatlasdev/ui, newest first. Follow it as an Atom feed (each entry carries the full post) or JSON.

4.5.0

@codeatlasdev/ui 4.5.0

  • i18n: the message catalog is split per group and each component bun...

1 changesetsrelease notes

4.4.0

@codeatlasdev/ui 4.4.0

  • NavItem in the AppShell panel gets color (a decorative dot), l...
  • AppShellLayout takes panelWidth (px, held between 160 and 480,...
  • Combobox: multiple turns it into a searchable multi-select (va...

27 changesetsrelease notes

4.3.0

@codeatlasdev/ui 4.3.0

  • Demo sidecars accept names (motion vocabulary) and motion (exac...
  • MotionStage and the MotionCtx type: the frame of a motion demo.

2 changesetsrelease notes

4.2.0

@codeatlasdev/ui 4.2.0

  • AI feedback loop.
  • Avatar renders the photo as a real <img> (no detached Image() p...
  • Card has a padding prop ('none' | 'sm' | 'md' | 'lg', any CSS...

22 changesetsrelease notes

4.1.0

@codeatlasdev/ui 4.1.0

  • Numbered demo catalog (ca-prompt): every docs demo has a stable ...
  • Checkbox, Switch and RadioGroup: on touch screens (pointer: coarse...

2 changesetsrelease notes

4.0.0

@codeatlasdev/ui 4.0.0

  • AppShellLayout renders its content region as <main id={mainId} t...
  • nativeFeel() now has one opt-out class, .ca-selectable, for bot...
  • caBrandPlugin writes src/generated/brand.ts as soon as the Vite...

21 changesetsrelease notes

3.1.0

@codeatlasdev/ui 3.1.0

  • .ca-prose styles release lists, so a changelog page needs no cust...
  • Add SwatchGroup: a single-choice color swatch picker (role="radi...
  • generateBrand and the brand Vite plugin no longer load every syst...

5 changesetsrelease notes

3.0.0

@codeatlasdev/ui 3.0.0

  • Sober default theme, Liquid Glass is now opt-in (#111).
  • AppShellLayout: the shell keeps its state across page loads (#109).
  • Brand assets from one mark.

5 changesetsrelease notes

2.3.2

@codeatlasdev/ui 2.3.2

  • DocsShell: the sidebar keeps its place between pages.

1 changesetsrelease notes

2.3.1

@codeatlasdev/ui 2.3.1

  • Theme islands: a data-theme-scope inside a data-preset island n...

1 changesetsrelease notes

2.3.0

@codeatlasdev/ui 2.3.0

  • The generated API reference now documents every non-component expor...
  • New .ca-sr-only utility (visually hidden, still in the accessibil...
  • Add the @codeatlasdev/ui/testing subpath: expectNoA11yViolations...

13 changesetsrelease notes

2.2.0

@codeatlasdev/ui 2.2.0

  • Accessible tokens and states, solved per preset and scheme by the g...
  • Component CSS now lives in @layer ca.components, declared last in...
  • i18n: open locales and request-scoped language (fixes #46).

5 changesetsrelease notes

2.1.1

@codeatlasdev/ui 2.1.1

  • Steps: a leading <strong>/<b> step title is a block, so its l...

1 changesetsrelease notes

2.1.0

@codeatlasdev/ui 2.1.0

  • Theme: port a palette by role and pick the page paper, without raw...

1 changesetsrelease notes

1.2.0

@codeatlasdev/ui 1.2.0

  • Design tokens v2: every visual dimension is a theme knob, so one co...
  • RadioGroup: variant="cards" (selectable cards with label and des...
  • Remove the legacy theme editor generator: editorTokensToCSS, edi...

4 changesetsrelease notes

1.1.0

@codeatlasdev/ui 1.1.0

  • Accessibility foundations.
  • Developer and agent tooling.
  • Performance foundations.

7 changesetsrelease notes

1.0.0

@codeatlasdev/ui 1.0.0

  • 1.0.0: stable API. From here on, breaking changes to props, exp...

1 changesetsrelease notes

All published versions

The list GitHub Releases shows right now, with every version's notes and anchors.

4.6.0 latest

npmGitHub release

Minor Changes

  • 0219812: AI agents get one canonical entry point. llms.txt opens with a recipe for every screen type (app shell, list, form, settings, auth, dashboard, landing, theming), then decision tables and a "common workarounds" page; get_guide and the new per-page files /llms/<slug>.txt carry the complete demo source of each page, while llms-full.txt stays under its old size. ca-ui-mcp adds get_recipe and reports deprecated components (Row, GlassCard, GlassSurface) with their replacement. init writes a marked, idempotent block into the app's AGENTS.md. The agent skill no longer teaches the removed cols/pixel-gap layout API or deprecated components.

  • 1b1b993: AspectRatio (#446, part of #432): a box that holds ratio for a video, an embed or any child with no size of its own; a direct img, video, iframe, canvas or svg child fills it. Image gains ratio (crop to a width / height box, filling the container) and fit (cover or contain). Replaces the Radix AspectRatio row of the React equivalence table.

  • 23fa9ee: Auth building blocks (#379): Turnstile in @codeatlasdev/ui/forms (Cloudflare captcha, explicit render, loads the vendor script on demand with no dependency and no server access, follows the colour scheme and the locale, bind:handle with getToken() / reset(), removed on unmount, silent no-op without a siteKey), PasswordStrength (3-step meter with a polite text label, pure passwordScore() and a score / labels override), SplashScreen (role="status", ready + minDuration, skipped under reduced motion) and Avatar decorative (no accessible name next to a visible name). New i18n groups passwordStrength, splashScreen and turnstile (en, pt-BR).

  • 00757fe: AuthLayout: panelTone (auto | brand | flat) and the --ca-auth-panel-bg / --ca-auth-panel-fg tokens. A flat panel is a solid surface with a hairline, themed text and no blobs; the default auto picks it under the flat and outlined surface styles, so themes that forbid gradients no longer override .ca-auth__panel and .ca-auth__blobs from app CSS (#409). The panel slot's --panel-* variables are now CSS (derived from --ca-auth-panel-fg) instead of an inline style.

  • 78ddb7f: AuthLayout: panelSide (left/right), panelBleed (full-bleed, sticky panel with no padding, logo or blobs, and a hairline divider), panelHidden (md 900px or lg 1200px; the panel snippet is unmounted while hidden, so media inside is never fetched on mobile), a header snippet (sticky bar over the form column that replaces the mobile brand strip) and align="start". AuthCard gets variant="plain" (display-size title, no card width cap). New template: Waitlist with map (#347).

  • b192b0a: Badge renders an inline status pill: <Badge tone="danger" subtle dot size="md">Overdue</Badge> (tones brand, success, danger, warning, info, neutral; subtle is a tinted background whose label clears 4.5:1 in light and dark). Button gains trailingHint (a Kbd in trailing is hidden below md and on touch with no reserved gap) and align="between" (label at the start, trailing at the end; the spinner keeps its place). Defaults are unchanged.

  • acdf4f3: ButtonGroup (#448, part of #432): attaches Button, IconButton and a DropdownMenu trigger into one role="group" control, horizontal or vertical (orientation), with square inner corners, a single-line seam and the hovered or keyboard-focused member raised over it. Replaces the shadcn Button Group row in the React equivalents table.

  • 8c31f6a: Button and IconButton get tone="neutral" (#357): outlined and ghost buttons drawn with a neutral label, border and hover/pressed fills instead of the brand colour, for secondary actions beside a brand filled one. IconButton also gets the outlined variant. Label and border contrast is checked for every preset in tests/contrast.test.ts.

  • 61912be: Carousel and CarouselItem (#445, part of #432): native scroll-snap slides that follow the WAI-ARIA carousel pattern (labelled region, slides named "n of m", off-screen slides inert, labelled dot buttons with aria-current, arrows / Home / End on the focused carousel). perView, loop, bind:index, onchange; autoplay (ms) is opt in, adds a Pause button, stops on focus or hover, is a polite live region only when stopped, and never runs under prefers-reduced-motion. No carousel engine dependency. Replaces shadcn Carousel. New i18n group carousel (en, pt-BR).

  • 82edb9d: ca-ui-mcp gains get_release(version): a release as a post for agents, with its highlights, the numbers and the conditions each was measured under, the behaviour changes to check after upgrading, the upgrade command and every merged change grouped by area with its pull request and issue. Without a version it returns the digest of every release. It reads dist/releases.json, built from the release posts in the docs site.

  • 7b30608: New ChipGroup: single choice among a few short options as chips that wrap (#355). A real radiogroup of native radios (roving tab stop, arrows/Home/End/Space/Enter, disabled items skipped), three columns below sm and free wrap from sm, a check icon on the selected chip, error as role="alert" with aria-invalid/aria-describedby on the group, name/required for native forms, 44px chips. Use it instead of RadioGroup (too tall) or SegmentedControl (never wraps) for options like team size.

  • 689d2c5: New token --ca-code-line-height (20px): an integer px line height for code surfaces. CodeSnippet, .ca-code pre and a bare Markdown pre inside .ca-prose use it instead of the unitless 1.6 / 1.7, which rounded to a different pixel height in a text layer and in an overlay (a caret and an active-line highlight drifting apart). The code inside a CodeSnippet also pins --ca-font-mono now: it used the UA's bare monospace, resolved per lookup. The docs gain "Code surfaces: integer line-height, one font stack" for apps that build an editor.

    The stylesheet also drops --ca-action-focus, a token no component, doc or template ever read, to stay inside its size budget.

  • caf82ad: Component tokens for Button, IconButton and Kbd. Weight, tracking, line-height, gap, per-size padding, height and font size, ghost border width, disabled background and the touch minimum are --ca-btn-*, --ca-icon-btn-* and --ca-kbd-* custom properties; set them on :root, on a wrapper or through the new ThemeOptions.components (button, iconButton, kbd, merged by mergeTheme). Unset, every component draws exactly what it drew in 4.5.0. One naming convention for all components (--ca-<component>-<property>, --ca-<component>-<size>-<property>) is documented in the theming guide. No more selectors against .ca-btn--lg, .ca-icon-btn or .ca-kbd.

  • 3b76318: ca-ui-mcp gains list_concepts and get_concept: the Concepts pages (viewport-complete UX, motion that respects people, targets and focus, appeal is not usability) with every claim's strength label, scope, sources, limits and last-reviewed date. The llms-full concept digest now carries the contract and the prompt only, so the file stays under its size cap.

  • e125cc8: ContextMenu (#455): wraps a region and opens a DropdownMenu at the pointer on right-click (a second one moves it), at the finger on a touch long-press (longPressDelay, default 500 ms, cancelled by lifting early or dragging) and from the keyboard with Shift+F10 or the ContextMenu key. The menu snippet takes the same items as DropdownMenu (items, checkbox and radio items, submenus, labels, separators). Text fields keep the browser's menu; disabled keeps it everywhere; bind:open, onOpenChange, focusable, menuClass.

  • c19ba2b: Density and appearance tokens for Avatar, TextArea, TextField, Card, SegmentedControl and EmptyState, on the same --ca-<component>-<property> convention as the Button tokens, also settable through ThemeOptions.components. Avatar gets tone="neutral" (surface background, secondary text, hairline ring) and --ca-avatar-fallback-{bg,color,size,weight} with per-size --ca-avatar-<size>-fallback-*. TextField and TextArea read --ca-field-gap and --ca-field-label-leading (or their own --ca-tf-* and --ca-ta-*); --ca-ta-min-height is the height of the visible box, border included. Card reads --ca-card-{bg,radius,border,shadow}. SegmentedControl reads --ca-seg-item-{padding-x,font-size,line-height} (and per size). EmptyState gets bare, actionsFill and --ca-empty-{padding,title-size,title-weight}. Unset, every component draws what it drew in 4.5.0.

  • ab42958: ca-ui-mcp gains search_terms, get_term and get_reference: the 269-term design atlas and the verified sources behind it, offline from dist/atlas.json. Scores are labelled heuristic, every term carries its limits and review date, and a reference returns what it does not support and the wording that was read.

  • 73f38cd: Design style pages (#583): /styles/<id>/ for swiss, editorial, brutalism, sketchbook, glassmorphism and terminal, each rendered in its own style, with the origin, the traits (type, colour, shape, depth, motion), do and don't, accessibility pitfalls with their sources, before and after on real components, the designStyle() recipe and a prompt. The ca-ui-mcp server gains get_style (a style as markdown, or the digest of all of them) and llms.txt links the digest at /llms/styles.txt. designDefaults() is exported: the stylesheet default of every knob a theme can write, so a scoped preview can reset what an outer theme set.

  • 300ac7c: Dialog split is configurable: splitSize={{ width, height }} (any CSS length), splitColumns="2fr 3fr" and fullscreenBelow="sm" | "md" (any size: exactly the viewport under the breakpoint, square corners, safe-area padding kept). They set the tokens --ca-dialog-split-width, --ca-dialog-split-height and --ca-dialog-split-columns. Defaults are unchanged. The insertion-line-free column drop requested in the same issue already ships as sortable({ drop: 'container' }); it now has a test for the announced target container on keyboard and touch.

  • ca5cc0e: Display text role (#350). New Heading component (level, size="display" | "display-2" | "display-3" | "h1".."h6") bound to --ca-font-display (the body font until fonts.display is set), the new --ca-display-tracking / --ca-display-leading tokens and the fluid --ca-font-size-display-* sizes. AuthCard, PageShell, EmptyState and SiteHeader take a display prop. defineTheme({ typography: { displayTracking, displayLeading } }) sets the tokens.

  • e5c7c0f: DocsHeader is the top bar of DocsShell as its own component, so the pages of a site that are not a docs frame (a landing page, a template gallery) render the same header as the docs: one height, container width, brand position, section tabs with an underline for the current one, search (⌘K), actions and theme toggle, and the same phone menu. The bar is now opaque (it was an 82% translucent fill with a blur), so content scrolled under it no longer shows through.

  • 4e4bd67: Drawer gets chromeless and fullBleedBelow (#362), mirroring Dialog's chromeless. chromeless renders no header, close button or footer actions and no body padding, so a detail view can bring its own header and scroll area; the panel is still named by title (screen readers only), aria-label or aria-labelledby. fullBleedBelow="sm" | "md" | "lg" makes a left/right panel exactly the viewport width and --ca-vh height below that breakpoint (no radius, border or page strip, safe-area padding kept). .ca-drawer__body is now a flex column with min-height: 0, so a child can own the scroll.

  • 2cc7983: Editorial primitives (#353). New tokens --ca-font-size-display-hero (fluid, 60-216px) and --ca-ease-out-expo / --ca-ease-out-quart / --ca-ease-in-out-quart (also in the easing export). New Highlight and StrikeThrough components with animated, drawn in their final state under prefers-reduced-motion. Surface takes grain (true | "animated"), a paper-noise overlay tuned by --ca-grain-opacity / --ca-grain-size. Fix: --ca-font-mono (and fontFamily.mono) now starts with the system monospace instead of JetBrains Mono, which the kit does not load; set fonts.mono to pick another.

  • 446c722: HoverCard (#454): a rich preview that opens on pointer hover (after openDelay) and on keyboard focus (at once), stays open while the pointer crosses into it (closeDelay), closes on Escape and blur, and is reachable with Tab from the trigger. A hint layer: it never takes Escape or presses from the layer below. Touch never opens it, so a tap only runs the trigger's own action (use Popover for a tap-to-open preview). triggerChild/trigger, bind:open, onOpenChange, side, align, sideOffset. FloatingLayer forwards onfocusout.

  • c8e9492: ca-ui-mcp searches the gallery index. search_ui takes words plus facet filters (kind, subtype, style, motion, interaction, layout, colorMode, density, tech, cost, component), list_facets lists every value with its count for the current selection, and get_related returns the entries related to a demo. They read the same published facets as api/v1/index.json: a facet still under review is never searchable.

  • a5bc3b7: init --write now produces a SvelteKit app that passes vite build, svelte-check --fail-on-warnings and codeatlas-ui lint as written, on Kit 2 and Kit 3.

    • PWA and SEO are opt-in. The manifest route, service worker, offline page, robots.txt and sitemap.xml are written only with init --with pwa (also combinable: --with lint,e2e,pwa). A plain init writes no manifest link in <Seo> (the old default failed the prerender with a 404 as soon as the manifest route was removed). --with pwa can be added later to an app init already set up.
    • Lint-clean output. The copy init writes (layout title, offline page, manifest name) lives in src/lib/messages.ts (export const m), so codeatlas-ui lint no longer reports i18n-literal on a freshly initialised app.
    • Kit 3 #lib/* map. init adds "imports": { "#lib/*": "./src/lib/*" } to package.json (never replacing an existing map) and imports the theme and messages through it, instead of relative paths.
    • Missing config files are created: vite.config.ts, src/app.html (without the inline-styled display: contents wrapper) and tsconfig.json instead of skip ... (not found). Kit 2's tsconfig.json gets allowImportingTsExtensions, noEmit and skipLibCheck, which its generated base config lacks; an existing file only gains the options it does not set.
    • @sveltejs/adapter-static: the adapter output folder (build, or the pages option) goes into the tsconfig exclude, so svelte-check no longer scans it, and src/routes/+layout.ts gets export const prerender = true;. init notes when the adapter major does not pair with the Kit major (@sveltejs/adapter-static@4 for Kit 3, @3 for Kit 2).
    • prerender.handleUnseenRoutes: "ignore" is written into the vite.config.ts that --with pwa creates on Kit 3 (no page links to /robots.txt or /sitemap.xml), and noted otherwise.
    • Fixed the public-origin note. Kit 2 rejects kit.paths.origin; the mechanism is kit.prerender.origin there and sveltekit({ paths: { origin } }) on Kit 3.
    • A re-run no longer mistakes the theme import init wrote into vite.config.ts for an app-owned theme.
  • a73cb68: inline on Dialog, Sheet, Drawer, Popover, DropdownMenu and Toaster renders the overlay in place instead of the top layer: position: absolute inside its container, never moved to <body>, no showModal() / showPopover(), no scroll lock, no focus trap, no inert page, no global Escape or outside-press ownership (Escape closes it from inside). It resolves the tokens of its container, so it follows a data-preset or data-theme-scope island. openLayer takes the same inline option. The Theme Studio overlays screen now shows the real components instead of static replicas.

  • d101e43: TextField addons, the InputGroup (#447, part of #432): prefix and suffix put text or a Button/IconButton/select inside the field border, flush with it and set apart by a divider. A string addon is read as part of the field's description and focuses the input on click; a snippet addon keeps its own name and Tab stop. The focus ring wraps the whole group; error, disabled and size="sm" reach the addons; --ca-tf-addon-bg sets the fill. .ca-tf__addon classes documented in docs/css-api.md.

  • 1263da3: New Kanban board (#361) on top of useDnd: columns with a fixed header (colour rail, title, count), independently scrolling card lists, a board that pans with the mouse and fades only where content is hidden, per-column onloadmore with skeleton cards and a "Load more (N)" fallback, a mobile tab strip (one column at a time) and a "Move to…" menu next to every card. Mouse, pen, touch and keyboard moves are announced. The engine gains sortable({ drop: "container" }) (column highlight, no insertion line, to.index always 0) and dnd.scroller() (horizontal board auto-scroll, faster the closer the pointer is to the edge); Left/Right move a card to the same index of the neighbouring column, an empty column included.

  • 46129ad: Kanban takes a summary snippet that renders per-column content (a money total, a weight) after the count while the colour rail, title and count stay; on the mobile tab layout it shows as a row above the active column's cards. header still replaces the whole title row. The kanban docs show a per-column money total and a board-height parent without :global.

  • 62a8d05: Kanban takes headingLevel (2–6, like EmptyState and FormSection) for its column titles (#501). The default level changes from <h3> to <h2> (inside a PageShell, the page's section level), so a board under the page <h1> no longer skips a level and passes axe heading-order. A board under an <h2> of its own passes headingLevel={3} to keep the old markup. Only the tag changes, never the look; a custom header snippet and the mobile tab layout (no column headings) are unaffected.

  • f6085f9: KpiCard takes a chart snippet (a Sparkline under the number, next to the icon) and a hint line, so an icon + sparkline tile needs no Card + Stat + Sparkline composition. The chart is a snippet rather than a data prop to keep the /charts subpath and echarts out of the core entry. New part: .ca-kpi__chart.

  • 25bd205: New layers store: layers.modalOpen is reactive and true while a modal layer (Dialog, AlertDialog, Drawer, Sheet) is open; layers.blocks(trigger) is the same but false for a trigger inside the topmost modal. Tooltip, HoverCard and NavigationMenuItem no longer open on pointer movement while a modal is open (unless their trigger is inside it), so a hover panel can no longer appear under a dialog. Gate your own hover or edge UI with the store.

  • d55e2a5: New LocaleSwitcher: a compact language selector for public headers. Options are data ({ value, label, short?, lang? }), each named in its own language with a lang attribute per item; onselect(locale) may return a promise (the switcher is busy until it settles, value changes only on success, onerror on failure); the trigger shows the native name from 600px and a short code below (or always with compact). Compose it in the trailing slot of SiteHeader, now documented as the end of the bar. New i18n group localeSwitcher (en, pt-BR) (#349).

  • c172f2c: New optional entry @codeatlasdev/ui/map for map products. MapStage wraps maplibre-gl (default) or mapbox-gl and keeps the basemap in step with the theme (styles: { light, dark }, swapped with setStyle on the same map), draws deck.gl layers interleaved through MapboxOverlay, fits bounds with padding, and turns flyTo into jumpTo under prefers-reduced-motion. MapControls adds glass zoom, compass (and optional geolocate) buttons with 44px touch targets. tokenColor(token, alpha?), mapColors(), useMapTheme() and rgbaCss() resolve oklch() / color-mix() tokens to [r, g, b, a] through a canvas, cached by resolved value so a theme change needs no manual reset. All engines are optional peers loaded inside onMount, so the stage renders in SSR. Pin @deck.gl/* to ~9.4.

  • 8607da1: Menubar and MenubarMenu (#456): the WAI-ARIA menubar for application menus. One tab stop; Left/Right/Home/End move between bar items (disabled skipped, wraps); Enter, Space or Down open a menu on its first item, Up on its last; inside an open menu Left/Right switch to the adjacent menu, while a submenu keeps its own arrows; Escape returns focus to the bar item; hover switches menus only once one is open. Menu content is the DropdownMenu items unchanged. DropdownMenu gains onmenukeydown, ontriggerkeydown and openFocus.

  • abb6269: Migration tooling per major (#382). migrations/<major>.json is the machine-readable record of every breaking change from 1.0.0 to 4.0.0 (before, after, why, and whether migrate covers it: yes, partial, no); it generates MIGRATION.md, dist/migrations.json and a new MCP tool get_migration(from, to). codeatlas-ui migrate --from 2 --to 4 [--write] [--report json|md] [--strict] runs only the codemods of the majors in that range, lists what it could not rewrite with file:line (Grid cols template strings, .ca-overlay selectors, setLocale, a bits-ui import, ...) and the changes left to review by hand, and exits 1 under --strict. It no longer answers a bare "Nothing to migrate": it names the codemods it checked. New codemod selectable-to-ca-selectable (4.0.0: class="selectable", class:selectable, .selectable in a style block). The dev warnings of GlassCard, GlassSurface, Row and setLocale on the server cite their migration id. A major changeset must now carry a Migration: <id> line naming an existing entry, and a test fails otherwise.

    NavModule is basePath/items in every release from 1.0.0 on: the path/sections shape in the old skill examples never shipped, so there is nothing to migrate.

  • 043c32e: Migration tooling for teams moving an existing app onto the library (#383). New commands of codeatlas-ui: tokens import <css|json> turns the custom properties of an existing design system (hex, rgb(), hsl() or oklch(), with or without alpha, one rule per theme) into a typed theme.ts with a preset per theme, composites every translucent token over its background and prints each token it flattened and each text or outline pair under 4.5:1 or 3:1 (--strict exits 1); doctor checks peer versions, optional peers, theme wiring, viewport meta, a root font-size, TODO(lib#n) markers and .ca-* selectors, one line per finding with a fix hint, and --fix renames the markers workarounds reads; lint is a UI guard that reads .svelte, .astro and .css (no hex, raw px, inline style, :global(.ca-*) outside ui-guard.config.json, literal text in templates); compare <urlA> <urlB> opens two pages in Chromium and prints the elements whose box, computed styles or pixels differ, sorted by deviation. init --with lint,e2e writes the guard scripts and a Playwright config with a 390/768/1280 matrix and an axe spec. migrate renames the pre-1.0 NavModule keys path/sections to basePath/items (registry entry v1-navmodule-basepath-items). @codeatlasdev/ui/testing gains withTheme({ scheme, preset, style }), coarsePointer() and measure(el, props).

  • 330d761: NavigationMenu, NavigationMenuItem and NavigationMenuLink (#457): disclosure navigation for marketing headers, not role="menu". A nav with a list of links and aria-expanded/aria-controls buttons that reveal a panel of links; one tab stop with Left/Right/Home/End across the top level, Enter/Space toggle, Down opens into the panel, Tab flows through the panel links and on, Escape returns focus to the button. Hover opens after openDelay, switches at once while one is open and closes after closeDelay; a click toggles (touch). current sets aria-current="page"; description adds a second line to a panel link.

  • 98a7396: PageFrame: page container for map-first apps. mode map | split | page, side, width sm | md | lg | xl, a non-modal bottom sheet below 900px with peek | half | full detents (drag or keyboard), padded, headerBorder and fixed header / toolbar / footer slots. New stageInsets store (and --ca-stage-inset-* on <html>) publishes the rectangle the frame covers so the stage can keep pins and routes clear of it; mode="page" sets data-ca-stage="hidden" on <html> (#344).

  • cbc214c: PageShell: new aside snippet (companion column, sticky beside the content from a 60rem-wide shell, after it below) with asideLabel, and width="reading" | "wide" to cap the content column at 46rem. AppShellLayout: new collapseShortPanel prop collapses the panel to the rail for modules with 2 destinations or fewer (#342).

  • 539c4be: Preset role colors take alpha, and a preset carries domain tokens. A role in colors.<scheme> is any CSS color (oklch(1 0 0 / 0.62), rgb(), color-mix(…)) or { color, alpha }, so a translucent text or border follows the surface behind it instead of being flattened to a hex; a value that is not one CSS color throws at build time, and caThemePlugin/caUI warn when a hand-set text, textSecondary or textHint is under 4.5:1 after compositing its alpha. PresetDefinition.tokens ({ status: { new: { light, dark } } }) emits --ca-token-status-new plus derived -subtle and -contrast per preset and scheme, with names typed through the CaTokenRegistry augmentation. tokenRgba("status.new") and onTokensChange(fn) (@codeatlasdev/ui/tokens) read any token as [r, g, b, a] within 1/255 of the CSS value, translucent colors included; the map entry's tokenColor now shares that resolver. #rrggbb presets behave as before.

  • 5eebf5e: Theme presets can own the primary-button and field roles: colors.<scheme>.brandFill, brandFillHover and brandContrast (--ca-brand-fill, --ca-brand-fill-hover, --ca-brand-contrast) and sizes.inputFontSize (--ca-input-font-size, still max(16px, …) on touch and below 900px). They are emitted with the same selectors as the other preset roles, so Button variant="filled" inside [data-preset="x"] needs no app CSS. caThemePlugin warns when brandContrast on brandFill is under 4.5:1 (caUI for Astro does too) (#354).

  • 70cce85: PreviewStage: a resizable device frame for showcasing a component or a whole page at 390, 768, 1280 or any width. A segmented width control and a keyboard-operable splitter set the frame width; a frame wider than the stage is scaled whole. Inline content is themed in its own ThemeIsland (scheme, preset); src shows a same-origin page in an isolated iframe.

  • c99fb14: Progress gets size="sm" | "md" | "lg" (4px, 6px, 8px; the default stays 8px) and honours a --ca-progress-height custom property for any other height (#358). Determinate and indeterminate bars both follow it and the ends stay pill-shaped.

  • 2ff23ff: AppShellLayout navigation for apps with many modules (#346): railLabels captions under rail icons, a railTop slot for a search / ⌘K trigger, NavModule.badge (number or 'dot') with badgeLabel folded into the button's accessible name, a bottom navigation that keeps bottomNavPrimary tabs and moves the rest into a "More" dialog by default (native <dialog>, bottomNavOverflow slot; opt-in moreSheet={NavMoreSheet} swaps in a full Sheet), and filterModules for role-based navigation (the active module is resolved on the filtered list). Rail takes top and labels, BottomNav takes primary and overflow.

  • 043c32e: Migrating a React app is now data, not prose (#383). equivalents/react-ui.json maps every shadcn/ui component (66) to the Radix primitive, Base UI package and MUI component that mean the same thing, the @codeatlasdev/ui target with its import path and prop renames, and, for the nine components the library lacks, the gap issue (#432) and a workaround. It is served by the new MCP tool get_equivalent("Dialog"), shipped as dist/equivalents.json, and generated into the migration guide, so the page cannot drift from the data. codeatlas-ui migrate-react lists every shadcn, Radix, Base UI and MUI import of a .tsx app with its replacement, and in an already-ported .svelte file renames className, onClick, htmlFor and the shadcn Button variants, reporting with file:line the props it cannot rewrite safely (onChange, onCheckedChange, asChild, spreads). The guide gains a Tailwind map and the React patterns that change shape (context, data fetching, routing, forms, SSR global state, per-request i18n), each with a type-checked Svelte block.

  • 5b50d09: The reset layer (@layer ca.reset) now gives every interactive shape a pointer cursor: button, a[href], label[for], summary, [role=button], [role=tab], [role=option] and [role=menuitem*]. :disabled and [aria-disabled=true] get not-allowed. A custom clickable tile no longer shows the arrow cursor; an app overrides the rule with an unlayered cursor declaration, no !important. The stylesheet drops the now redundant cursor: pointer of .glass-btn* and .ca-rail-btn, the unused .ca-bar__actions rule and the unused --ca-grid-columns token to stay inside its 17250 B budget.

  • 5931a06: ResizablePanels, ResizablePanel and ResizableHandle (#444, part of #432): panels split by WAI-ARIA window splitters. Sizes are percentages that add up to 100; a handle trades only with its two neighbours and stops at each panel's minSize / maxSize. Arrow keys move 1%, Shift 10%, Home and End go to the limits (Up and Down in a direction="vertical" group, mirrored in RTL). persist keeps the layout in localStorage and ignores a stored layout that does not match the panels; onlayout reports it. Replaces react-resizable-panels / shadcn Resizable. New i18n group resizablePanels (en, pt-BR).

  • 194723a: AppShellLayout stage snippet (#343): a persistent background layer (map, canvas, video) rendered once behind the pages, so it keeps its state across route changes. The content region passes pointer events through (panels opt back in with .ca-stage-panel), the shell paints nothing behind it, and stageVisible={false} or data-ca-stage="hidden" on <html> hides and pauses it without unmounting. The shell exposes --ca-shell-inset-top|right|bottom|left (rail and panel, mobile header, bottom navigation and safe area).

  • b46a5f7: SimpleTable gets a dense, spreadsheet-like mode (#360): density="compact" (rows up to 40px), frame={false}, scroll="none" (the caller owns scrolling, so the sticky header sticks to your page or panel), stickyColumns={n} (pinned columns keep an opaque background in hover, selected and focus states) and headerCase="sentence". New TableRow (selected, onactivate: Tab, Enter/Space and click, nested controls never bubble), TableGroupRow (collapsible, aria-expanded, sticky label) and TableSortHeader (sort, onsort, aria-sort).

  • b8edc32: New optional entry @codeatlasdev/ui/sound: opt-in, synthesized interface sound with no audio files (under 4 KB gzip). One shared AudioContext is created only inside the first pointer or key event after the user turns sound on with <SoundToggle /> (aria-pressed, accessible name, optional volume slider); the choice persists in localStorage key ca-sound and sound is off by default. Every cue is a short oscillator and noise recipe behind four profiles (system, glass, brutal, terminal) that follow the design style through --ca-sound-profile (written by designStyle()), tunable with --ca-sound-gain, --ca-sound-pitch and --ca-sound-brightness. Hover and slider tick cues are off under prefers-reduced-motion; sound is suspended while the tab is hidden or media plays, with one cue per name per 40 ms and at most four voices. Button, Switch, Toast (toast.*(msg, { sound })), Dialog, Tabs and Slider take a sound prop (boolean | CueName) that emits through soundBus, a no-op hook exported from the root, so core stays silent without the entry. use:sound (the sound export is also the action) plays a cue on any DOM event. Docs: Feedback > Sound, and the Sound principles guide.

  • b476462: Official list of stable override hooks (#381). The manifest gets a styling section per component (components.json): stable ca-* parts, internal parts (ca-x-* classes and @keyframes names, which can change in any release), component-scoped --ca-* variables and the data-* attributes its CSS selects on. It is surfaced in API.md, in dist/parts.json and in a new MCP tool list_parts; get_component returns it too. codeatlas-ui workarounds now also scans .css, .svelte and .astro for .ca-* selectors that are not a stable part, and --strict exits 1 on them (a ca-ui-workaround(#n) marker on the line or the one above excuses it). A test fails when a documented part is no longer applied by any markup; it found and removed the dead .ca-date__sep rule.

  • 3b50a5c: New Stat (#359): a card-less statistic with label, value, unit, hint, trend, size (sm | md | lg), align and skeleton. It renders a description list (label <dt>, value <dd>), has no surface, border or padding, uses tabular numerals, truncates the label and never wraps the value. KpiCard is now a Stat inside a Card, so both share one typography: the doubled inner padding (card padding + .ca-kpi__inner padding) is gone, and the unused .ca-kpi__top and .ca-kpi__value-row wrapper classes were removed; .ca-kpi__title, __value, __unit, __trend(--up/--down) and __icon stay.

  • b34ffd4: Stat and KpiCard take trendTone ('positive-up' | 'positive-down' | 'neutral') so a trend can be good news when it falls (churn, cost, latency). The arrow still follows the sign, the colour follows the tone (hook: data-tone="good|bad|neutral" on .ca-stat__trend) and the accessible label says better or worse (Trend: -1.8%, better, Tendência: -1.8%, melhor). Without a tone nothing changes. Apps that dropped trend and wrote the delta in hint can pass it again.

  • 7bc89c7: TextField: revealable adds a show/hide eye button to type="password" (aria-pressed, localized name, 44px hit area on touch, no layout shift), below renders a snippet under the field and its hint or error (a "Forgot password?" link, a strength meter) and requiredIndicator={false} hides the asterisk of a required field. New i18n group textField (en, pt-BR) (#348).

  • 3aacf62: ThemeIsland: a block or a whole page with its own preset and scheme. document also mirrors data-preset, data-theme-scope and the dark class onto <html> while mounted (so html/body, overscroll and color-scheme match) and restores the app's values on unmount, with overlapping islands and theme-store writes handled. SSR parity: setRouteTheme(event, { preset, scheme }) in a load, or caThemeHandle(theme, { routes }) by route id prefix, writes the island on <html> in the server response, and the init script and the Astro <ClientRouter /> swap keep the app's own preset aside for the navigation back. The docs page explains how nested presets and data-theme-scope resolve (#352).

  • c39a426: New ColorField (swatch, native picker, #hex/rgb()/oklch() entry and a live WCAG contrast readout) and SliderField (label, slider and an editable number with a unit). Slider takes a label so a thumb is announced as "Radius" instead of "Value 40". The color helpers behind them are public: parseColorInput, contrast, wcagLevel, toHex, toOklch, formatOklch, extractPalette and pickBrand (derive a brand from the pixels of a logo), plus presetContrastIssues and presetTokens for theme editors.

  • 37cda52: toast: optional description (secondary line) and a single inline action: { label, onClick } on toast.show and the success/error/warning/info shortcuts, which now also take the options object as the second argument (positional duration still works). Clicking the action runs it and dismisses the toast; an actionable toast lasts 8 s by default. The auto-dismiss timer pauses while the toast is hovered or focused and resumes with the time left (toast.pause(id) / toast.resume(id)), and Escape dismisses the focused toast. Errors and warnings are now role="alert", the others stay role="status". The action is a text button with a 44px touch target on touch devices.

  • 15074bf: A named type scale by role. title, heading, body, label, caption and num are size / leading / tracking / weight bundles (--ca-text-<role>-size|leading|tracking|weight, and --ca-text-<role> as a font shorthand), applied with <Text role> or .ca-text-<role>; num renders tabular digits so a ticking counter never shifts its neighbours. ThemeOptions.type = { label: { size: "13px", leading: 1.25, tracking: "0", weight: 500 } } overrides any part for the whole app, library components included: --ca-font-size-label, -body1, -caption and --ca-label-tracking are now derived from the roles, with the same default values, so existing components follow without edits. Unknown roles and values that are not a length, number or keyword throw at build time.

Patch Changes

  • 817b5c4: get_recipe, llms.txt and the decision tables now teach the 4.6 APIs: a map-first recipe (AppShellLayout stage, MapStage, PageFrame), rail labels and the "More" sheet, ChipGroup, Stat, dense tables, Kanban, Drawer chromeless, toast actions, tone="neutral", ThemeIsland and per-preset button colors. The "common workarounds" page lists what each one replaces.

  • caf1fa7: AppList is generic: T is inferred from data, so the list of its filters, children and empty snippets is a UseListStateReturn<T> (rows are typed, searchKeys and idKey are checked against T) instead of any, and a snippet annotated with UseListStateReturn<User> type-checks. The recipes now keep app copy in a typed src/lib/messages.ts, so they pass codeatlas-ui lint; the new App copy page explains the i18n-literal rule and that pattern.

  • 7667a22: AppShellLayout: only the most specific nav item is active. With items at /items and /items/a, visiting /items/a marks just Item A (aria-current, active style, section auto-open) and the mobile header title is its label, no longer the overview's. Same rule as modules: longest path wins whatever the order. The overview item stays active on its other sub-paths. findActiveItemPath follows it and the new findActiveItem(entries, pathname) returns the item itself.

  • 2ccbb9f: caUI (Astro) now puts the theme first in <head>, ahead of every stylesheet. It used injectScript("head-inline"), which Astro renders AFTER every bundled <style>: with inlineStylesheets: "always" the init script sat behind hundreds of KB of CSS and ran only once the browser had parsed it, so a slow link could paint the wrong theme first. A middleware now rewrites the rendered page, at build time and in SSR and dev alike, so the order holds whatever the layout or the bundler does. Font preloads move ahead of the CSS too. caUI(theme, { head }) takes extra first-paint markup (a boot script) that lands right after the theme. caThemeHandle and caThemePlugin already emitted at the start of <head>.

  • 6d65542: AuthLayout: a brand snippet now replaces the whole logo. The default "CodeAtlas UI" name no longer renders beside it, in the side panel or the mobile strip; pass logoName to show a name next to your brand (#340).

  • 06fb8aa: AuthLayout: on a flat panel (panelTone="flat", or auto under the flat and outlined surface styles) --panel-text-secondary and --panel-text-tertiary are the theme's --ca-text-secondary and --ca-text-hint instead of alpha mixes of the foreground, which measured 4.39:1 and 2.4:1 in dark and 2.21:1 in light over the raised surface. The brand gradient keeps the alpha levels.

  • 2b04e94: AuthLayout: the compact mobile brand mark now sits on --ca-brand-fill (the surface --ca-brand-contrast is paired with) instead of --ca-brand-main, so its label holds 4.5:1 in dark on every preset. It failed color-contrast in dark at 390px on six of the eight built-in presets.

  • 1cf8c69: --ca-brand-text is now solved per preset and scheme instead of aliasing --ca-brand-dark/--ca-brand-light. A light brand (lime, yellow, light cyan) no longer gets a 1.6:1 label on outlined and ghost buttons, links and active nav: the shade is darkened (light scheme) or lifted (dark scheme) along the brand hue until it holds 4.5:1 on every surface and on the hover/selected fills. Built-in presets keep their light value; dark values lift slightly where they were under 4.5:1. tests/contrast.test.ts measures every preset, light brands and colors.<scheme>.brand.

  • 35b5c9b: <Button type="submit"> and <Button type="submit" form="id"> type-check in apps without strict (the SvelteKit 2 scaffold): svelte-check widens a static type="submit" to string, which matched neither branch of the Button props union (form was never). The button branch now accepts the widened string. Documented on Button and Dialog: a submit button in a Dialog actions footer names its form with form.

  • ad005c0: Card and GlassCard: the header no longer adds its own padding on top of the card padding, so the title shares the left edge and top spacing of the content (#341). flush / padding="none" cards keep the header inset. New Card prop gap ('sm' | 'md' | 'lg' | length | number) stacks several inline controls (Checkbox, Switch, TextField) in a column.

  • 419d775: Carousel: the slide strip is now the focusable region (axe scrollable-region-focusable; arrows, Home and End work while it has focus, also when no slide holds a control), it no longer widens a flex or grid slot to the slides' width, and previous / next / dots reach the 44px touch target. Menubar items get the 44px hit area under a coarse pointer. TextField can shrink inside a grid or flex slot narrower than the input's intrinsic width (addon fields overflowed phones).

  • c39a426: Charts follow a theme scope. They read their colors with getComputedStyle on <html>, so a chart inside a ThemeIsland or any data-preset / data-theme-scope element painted the page's colors, and its series palette ignored the island's preset. ThemeIsland now provides its element as the scope, and the new ChartScope (from @codeatlasdev/ui/charts) does the same for your own scoped containers. Without a scope nothing changes.

  • 73f38cd: CodeSnippet highlights any language shiki bundles (markdown, yaml, diff, ...), loading it on first use, and falls back to plain text for a name it does not know. Before, a language outside the eight preloaded ones made the highlighter throw inside the promise and the snippet stayed on its unhighlighted fallback for good.

  • 73f38cd: CodeSnippet: a code block that scrolls sideways is keyboard-reachable. The raw fallback shown before the highlighter loads (and the highlighted block) now use scrollFocusable, so a long line no longer leaves a scroll region that only a mouse can reach (axe scrollable-region-focusable).

  • 475158b: The stylesheet drops 12 utility classes that no component, template or documented API ever used: .glass-bg-extend, .glass-inner-sm|md|lg, .glass-prominent, .glass-scroll-edge-top|bottom, .ca-toolbar-sep, .ca-clear-btn, .ca-row-check, .ca-trend-up|down. styles/global.css shrinks by 242 B gzip (17458 B to 17216 B) and its size budget goes back from 17500 B to 17250 B. An app that styled an element with one of these classes keeps the rule in its own CSS.

  • 6aafbb5: A page with no data-style on <html> now renders as data-style="solid", the package default: no backdrop blur, opaque surfaces, no glow, the pulsing skeleton. Before, an app without the theme plugin (no themeScript, no caUI) kept the glass tokens, so every data-ca-blur element (chips, tabs, toasts, rail, panels) blurred. Glass, frosted and the other styles are unchanged: set data-style to opt in.

  • ce8ed51: Docs, skill, AGENTS.md, README, MCP list_surfaces and component JSDoc present the library as theme-driven with style as one knob (default solid) and glass as an opt-in style. Glass-only rules (backdrop-filter, data-ca-blur, will-change) are scoped to a "Glass and frosted styles" section with their wording unchanged. Examples, templates and demo specs no longer pin material="glass". The liquid-glass npm keyword is replaced by theming and design-tokens. No API changes.

  • 866912f: FileUpload: the drop zone's accessible name now includes the visible hint (aria-labelledby over the label and the hint), fixing the axe label-content-name-mismatch / WCAG 2.5.3 failure (#490). With a custom children snippet the zone keeps aria-label.

  • fbc0d75: The gallery filters by facet. Chips for kind, subtype, style, motion, interaction, layout, colour mode, density, tech and cost narrow the cards instantly, show how many entries each value would match, and drop combinations that match nothing. Words match titles, aliases, tags and components. Facets, words and the group live in the URL (?kind=motion&style=aurora&q=fade), so every filter is a shareable link. Only published facets are offered.

  • 99a3dbd: A gallery item opens on Preview, Code, Prompt and Anatomy tabs (?tab=prompt, ?tab=anatomy). Prompt is the text to hand an AI: the get_demo call, the library parts, the behaviour to keep, the exact motion timings and the reduced-motion and keyboard promises read from the index. Anatomy lists the components with their package and docs link, how the demo is built, and related items. Pages show Preview and Prompt.

  • 678d673: init --write on SvelteKit 2 with @sveltejs/adapter-static creates svelte.config.js with the adapter and vitePreprocess, or adds adapter: adapter() and its import to an existing config that names no adapter. Before, vite build ran with no adapter and no build/ output. A config that already sets adapter is untouched; Kit 3 is unchanged (the adapter goes into vite.config).

  • 7fe318d: init on SvelteKit adds "prepare": "svelte-kit sync" to package.json when the app has no prepare script, so svelte-check on a fresh clone no longer stops with File '$app/tsconfig' not found. src/lib/messages.ts carries only appTitle unless --with pwa is given: the offline keys are written (or added to the file init wrote) only with the offline page that reads them.

  • b8e217e: Surfaces and chips inside a ThemeIsland (and a PreviewStage with a scheme or preset) follow the island, not the page around it. The layer tokens (--ca-overlay-bg, --ca-panel-bg, --ca-chrome-bg, --ca-upload-bg) and the solid style's tint overrides are re-declared on [data-theme-scope], so a dark island on a light page no longer inherits the page's resolved light surface (a FileUpload zone, an AppForm footer) under dark text. The success, error, warning and info chip text follows [data-theme-scope] as well as .dark.

  • fe9c5d3: Kanban no longer collapses to 0px wide inside an auto-width flex wrapper (a wrapping flex row, an island): the size-contained board now has a one-column floor width (contain-intrinsic-inline-size), while a narrower parent still shrinks it and the columns never widen the page (#530).

  • 7873a89: The Landing page recipe links only to in-page anchors and to /sign-in and /sign-up (the Auth pages recipe), so copying it whole prerenders with adapter-static; before, /pricing, /docs and /contact failed the build with a 404.

  • 85086c5: The Landing page recipe now prerenders when copied on its own: the sign-up and sign-in buttons and the plan CTAs use signUp/signIn constants at the top of the page that are in-page anchors (#pricing), instead of linking to /sign-up and /sign-in, which 404 the prerender unless the Auth pages recipe is also in the app. Point the constants at the real routes once they exist.

  • 82edb9d: The close button of Dialog, AlertDialog, Drawer and Sheet (.ca-layer-close, drawn 28px) gets the invisible 44px hit area under a coarse pointer, like every other small control. It was missing from the touch list, so an overlay measured 27 by 27 on a phone, inline overlays included.

  • b8edc32: A tooltip or hover card that was open when its trigger opened a dialog no longer leaves a second, stray copy in the page after the dialog closes. The layer stack put a hosted hint back at its old position even when the dialog had already been removed with the hint inside, resurrecting a node its owner had dropped (no popover, in normal flow, same id).

  • 503a842: init --write adds the "imports": { "#lib/*": "./src/lib/*" } map to package.json and writes #lib/theme.ts / #lib/messages.ts imports on SvelteKit 2 as well as 3 (Kit 2's Vite and TypeScript resolve the subpath import, verified by building and type-checking a Kit 2 app). The recipes, app-copy, installation and PWA pages now use the one spelling #lib/...ts on both majors instead of telling Kit 2 users to rewrite every import to $lib. An existing $lib import on Kit 2 keeps working and is never rewritten.

  • 9e751a2: llms.txt and the AGENTS.md block written by init say that dist/components.json keeps the components under .components[] and every other export under .exports[].

  • 3b76318: llms-full.txt keeps only the props table of each non-core page. The summary also carried the prose and examples that follow a table (the SEO page alone added 9 KB), which pushed the file past its size cap once the Concepts pages were added; the descriptions stay in each page's own file.

  • ce45681: llms.txt links the agent-readable .txt form of every docs page (/llms/<group>/<slug>.txt, index pages /llms/<slug>.txt) and of each recipe (/llms/recipes/<id>.txt), with the HTML URL beside it, and states the fetch rule at the top. dist/components.json (and get_component) now list the layout token props of Flex, Stack, Grid, Box, Container, Section and Surface (gap, align, justify, direction, wrap, columns, p, px, py) from their LayoutProps pick.

  • b8edc32: LoadingBoundary holds the placeholder for the full minDuration: the hold re-checks the clock when its timer fires and re-arms for the remainder, instead of ending up to a millisecond early when a timer fires ahead of performance.now().

  • de1c873: components.json (exports[].members), API.md and the MCP get_export now list the fields of every exported interface (NavModule, NavItem, NavSection, option and state types) with type, optionality and JSDoc, instead of one collapsed line.

  • 573950f: components.json, API.md and the MCP get_component spell out the values of a prop typed with a literal-union alias (Avatar shape: 'circle' | 'rounded', size, status, Card headingLevel: 2 | 3 | 4 | 5 | 6, Button variant and about 100 more) instead of the bare alias name, and API.md keeps the description of each snippet (SkipLink children replaces the label).

  • 48e1d42: @codeatlasdev/ui/map: MapStage works with maplibre-gl 6, so the peer range is ^5.0.0 || ^6.0.0 again and you can take the fix for the critical attribution-sanitizer XSS in maplibre-gl 6.4.0 and earlier (GHSA-jrc7-96c5-q579, fixed in 6.4.1). With engine="maplibre" the deck.gl layers now go through MapLibreOverlay from the new optional peer @deck.gl/maplibre (~9.4.0, which supports maplibre-gl 4.5.1, 5 and 6) instead of MapboxOverlay, which throws Cannot read properties of undefined (reading 'height') on every frame there. engine="mapbox" keeps @deck.gl/mapbox. Install @deck.gl/maplibre instead of @deck.gl/mapbox when you use maplibre-gl. On maplibre-gl 6 the app must also call setWorkerUrl once (Vite: import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url"), as MapLibre's v6 migration guide requires for every bundler; the Map page shows it.

  • b53e1c2: MapStage: fitting clear of a wide PageFrame no longer throws Invalid LngLat object: (-Infinity, NaN). The stage set the map's padding and passed the same padding to fitBounds, which subtracted it twice and left a wide frame no area; the bounds now fit inside the visible area exactly once (#407).

  • 0169158: nativeFeel() no longer forces the arrow cursor on buttons, links, tabs, menu items and options. html[data-ca-native] set cursor: default in ca.base, which outranks the pointer cursor of ca.reset, so an app shell never showed the hand on clickable controls (nor not-allowed on disabled ones). The rule keeps blocking text selection and the touch callout on chrome; the cursor now comes from the reset layer alone.

  • b8edc32: NavigationMenu: Enter or Space on a trigger always toggles its panel. The click grace that lets a pointer click confirm a hover-opened panel also swallowed a keyboard activation made within 600 ms of a hover open, so a keyboard user could not close the panel.

  • 2ccbb9f: The theme init script no longer fails with the page. When localStorage throws on access (private mode, blocked cookies, a sandboxed iframe) it used to abort before setting data-preset, data-style and the dark class, so the page painted unthemed and then switched once the store hydrated. It now falls back to the theme's defaults on the first frame, and a failing removeItem while discarding a corrupt value no longer stops the rest of the script. A size-limit entry pins the emitted script (under 1 kB gzip).

    colorScheme.setTokens now actually applies in dark mode with a brand preset. Runtime tokens were written as :root{…} (specificity 0,1,0) and lost to the preset's .dark[data-preset] block (0,2,0) whatever the source order, so the stored tokens replayed by the init script, and the ones the store wrote, silently did nothing. Both now use :root:root:root{…}.

  • 8e950af: NotificationBell: the trigger's accessible name now starts with the visible unread count (2 unread notifications / 2 notificações não lidas, plain Notifications at zero), so it contains the visible badge text (WCAG 2.5.3, axe label-content-name-mismatch). Custom notificationBell.label overrides should keep the count in the name.

  • ae3b5bd: PageFrame: on mobile (<=900px) the full-screen panel and the bottom sheet no longer overflow the viewport by the frame gap with side="left". The desktop data-side position rules out-ranked the mobile inset: 0 reset, so the panel kept left: 12px with width: 100% (right edge 402px on 390px). Fixes #489.

  • 57ff10e: PageShell: below 900px only Buttons in actions stretch across the row. StatusChip, Chip, Badge and plain text keep their natural width instead of becoming a full-width bar (#339).

  • e5cb35d: PageShell and PageFrame warn once in development when the page has no <main> landmark (no main/[role=main] ancestor and none in the document), pointing at the Standalone page recipe. Silent inside AppShellLayout, AuthLayout, DocsShell or your own <main>. llms.txt lists Standalone page first under "Start here".

  • 99ab5db: .ca-prose and .ca-code fences: the code inside a pre inherits the pinned --ca-font-mono stack instead of the browser's bare monospace, which Chromium could resolve to a different font (and line height) between two paints.

  • 0168dcf: Theme: --ca-font-size-body1 no longer sets the root font-size. :root keeps the kit's rem base (calc(0.9375rem * var(--ca-font-scale)), 15px by default, still relative to the user's browser size), so default rendering is unchanged and overriding body1 only sizes the text on body. A denser body no longer shrinks labels, inputs, buttons and captions (#351).

  • b8e217e: scrollFocusable re-measures when content is added at any depth, not only as a direct child of the scroller. A fixed-height wrapper never resizes when the content it holds grows, so a lazily loaded demo inside a PreviewStage frame left its scrollable region without a tab stop.

  • b8e217e: PreviewStage frames and ResizablePanels that scroll are keyboard-reachable while their content overflows (scrollFocusable, WCAG 2.1.1): a demo taller than the stage, or a narrow panel whose content is wider than it, no longer traps its content out of keyboard reach. Without overflow they add no tab stop.

  • b8e217e: SegmentedControl items are at least 44px wide as well as tall under a coarse pointer: in a narrow slot the equal shrinkable columns squeezed short labels (a "390" preset) to 38px, under the touch target.

  • 03c5f65: The Settings page recipe no longer tells a Switch inside a SettingItem to carry its own aria-label: the row is a <label> and already names it (as the SettingItem page says), so the redundant attribute and the rule are gone.

  • f45caa6: SiteHeader: a NavigationMenu placed directly in trailing is hidden below 600px, and trailing can shrink (min-width: 0). A marketing bar with three nav links, a LocaleSwitcher and two buttons overflowed the page horizontally at 390px (right edge 437px); the locale switcher and the buttons now stay in the bar and the links belong in a mobile menu of your own (#476).

  • 169db3e: toast: identical messages no longer stack. toast.error("x") ten times leaves one toast with a ×10 badge and a restarted timer, at most three toasts show at once (the oldest is dropped), and { id } replaces a specific toast in place. The shortcuts (success, error, warning, info) also accept an options object ({ duration, id }) as the second argument; positional duration still works. ToastItem gains count and ToastOptions is exported.

  • 9a10232: Touch targets: the password reveal button (TextField revealable) gets its 44px hit area on touch again. A merge had fused two selectors in the coarse-pointer list (#417).

  • b8edc32: VirtualList no longer drags the user back down after older rows are prepended. A prepend always takes its own scroll anchor, and a restore that was waiting for its tick when the prepend landed no longer applies the anchor it captured against the old offsets (the list jumped to the old first row, about 20 rows down, and skipped the next onloadmore).

  • cb6da91: VirtualList: a scroll made while the anchor restore after a prepend or a row re-measure is still queued is no longer undone. On a slow machine the late restore dragged the list back to the old anchor, so a user scrolling to the top to load the next page was thrown ~1000px down.

4.5.0 latest

npmGitHub release

Minor Changes

  • c23cff4: i18n: the message catalog is split per group and each component bundles only its own strings. root: Button only drops from 27.89 to 23.91 kB gzip and AppShellLayout from 39.88 to 36.07 kB. Messages, getMessages, useT, I18nProvider, setLocale and registerLocale behave as before; useT() and getMessages() still return the full catalog, which is bundled only when you import them.

4.4.0 latest

npmGitHub release

Minor Changes

  • 1c6653e: NavItem in the AppShell panel gets color (a decorative dot), leading (a snippet that replaces the icon), count (a neutral total, announced as a bare number instead of "N unread", 1.2k above 999) and actions (a snippet of DropdownMenuItems behind a "…" button named "Actions for <label>") (#275). NavSection.onReorder and NavModule.onReorder make the rows reorderable with the useDnd primitive: a grip per row, mouse, touch press-and-hold and keyboard, with announcements; the shell reports { id, from, to } (new NavReorderEvent type) and never reorders items itself. PanelNavItem now renders a row that contains the <a class="ca-nav-item__link"> (a link cannot hold the actions button or the grip), so .ca-nav-item is the row, not the anchor. New messages appShell.itemActions and appShell.moveItem.

  • 1c6653e: AppShellLayout takes panelWidth (px, held between 160 and 480, default 200) and resizablePanel (#292). The content offset and the open and close animation follow the width, so a panel for long labels no longer misaligns the page. resizablePanel adds a keyboard-operable drag handle on the panel's trailing edge (Arrow keys 8px, Shift 32px, Home and End for the bounds); with persistPanel the chosen width is stored in ca-shell and restored by the inline script before the first paint. New appShell.resizePanel message and .ca-panel__resize class.

  • ca80cfd: Combobox: multiple turns it into a searchable multi-select (value is string[], aria-multiselectable listbox, list stays open, removable chips, Backspace on an empty query removes the last chip) and creatable works in both modes. CommandPalette: search(query, signal) adds debounced (searchDebounce), abortable server-side results with loading, empty and failed states announced through announce(), plus onSelect and the exported CommandResult type.

  • 49116d1: DataTable gains row groups, column resize, row reorder and safer interactive cells (#283). groupBy renders one <tbody> per key with a collapsible header (aria-expanded, groupLabel to customize). meta.resizable on a column adds a window-splitter handle (mouse, touch, keyboard arrows, Shift, Home, End, Enter) driven by size, minSize and maxSize; the new DataTableColumnResize is exported for hand-built headers. onReorder adds a drag grip per row on top of the new useDnd primitive, moves rows between groups and reports { id, from, to } (DataTableReorderEvent); rowId gives rows stable ids and pageSize sets the page size in simple mode. A click on a link, button, input or other control inside a cell no longer fires onRowClick (add data-no-row-click to opt other elements out). The new labels (Reorder, Move, Resize) follow the active locale (en, pt-BR). Needs the drag and drop primitive from #284.

  • d138bd8: New drag and drop primitive, useDnd(), applyMove and <DndLayer>, for sortable lists, moves between lists and drop zones (#284). It works with mouse and pen (4px threshold), touch (press and hold, so the page still scrolls) and keyboard (Space lifts, arrows move, Home/End jump, Enter drops, Escape cancels), speaks every step through announce() (English and Portuguese built in, useDnd({ messages }) for others), auto-scrolls near the edges of a scrolling container and drops its animations under prefers-reduced-motion. The engine measures the boxes once per drag and only moves a clone, a drop line and data-dnd-* attributes, so the list does not re-render until onDrop. <DndLayer {dnd} /> renders the keyboard instructions and the forced-colors aware styles (.ca-dnd-overlay, .ca-dnd-indicator), so apps that never drag add no bytes to core.css or to the i18n catalog.

  • 946d69d: Drawer can be named without its default title: pass aria-label, aria-labelledby (your own header) or hideTitle, and id, data-* and other attributes now reach the <dialog> (#285). AlertDialog gains confirmProps and cancelProps (attributes for the two buttons) and cancelVariant (#287), plus a children body, a description that accepts a snippet and confirmDisabled for type-to-confirm (#278).

  • e983caa: DropdownMenu is controllable and can open at a point. bind:open and onOpenChange read and set the open state; anchor={{ x, y }} opens the menu at a viewport point (a right-click menu, no trigger needed) and changing it while open moves the menu instead of toggling it. A menu opened from code starts on its first item and returns focus to the element that had it. On the built-in trigger Shift+F10 and the ContextMenu key open the menu like ArrowDown.

  • e983caa: DropdownMenu gains selection items. DropdownMenuCheckboxItem (role="menuitemcheckbox", bind:checked, onCheckedChange) and DropdownMenuRadioGroup + DropdownMenuRadioItem (role="menuitemradio", bind:value, onValueChange) render aria-checked and a check indicator, stay open after a pick (closeOnSelect closes), and join arrow, Home, End and typeahead navigation (the menu now also recognizes menuitemradio).

  • e983caa: DropdownMenu gains submenus. DropdownMenuSub takes a trigger snippet holding a DropdownMenuSubTrigger item (aria-haspopup="menu", aria-expanded, chevron) and the submenu items as children; they nest to any depth. ArrowRight, Enter or Space opens the submenu on its first item, ArrowLeft or Escape closes only that level, hovering the trigger opens it after a short intent delay and the pointer can cross into it. Selecting an item closes every level. A popover layer that is closing no longer owns Escape or outside presses, so a closing submenu hands them to its parent.

  • ff460ad: Form controls forward native attributes and expose their element, with one vocabulary (ref, hideLabel, rest props on the native element):

    • Checkbox, TextField, TextArea and FileUpload spread every other attribute and handler (data-*, maxlength, inputmode, onclick, onkeydown, name, capture...) on the native <input>/<textarea>, and take a bindable ref with that element. Checkbox onchange now receives the native event as a second argument, and its forwarded onclick makes Shift range selection possible. TextField/TextArea gain onfocus and hideLabel (label kept for screen readers with .ca-sr-only; aria-label also works without a label).
    • Select accepts aria-label and aria-labelledby for a compact select without a visible label. The trigger is named by that text followed by the current value (visible text stays in the name); without either, the placeholder names it as before.
    • FileUpload gains ref, an open() instance method for a button outside the dropzone, and list={false}: a plain picker whose onchange receives only the files just picked, with no built-in list and nothing kept in files (the native input is cleared, so the same file can be picked again).
  • 8c24c31: Theme option motion: { enter: "none" } (token --ca-enter-scale: 0) turns off entrance and exit animations only: Dialog, AlertDialog, Drawer, Sheet, Lightbox, CommandPalette, DocsSearch, date pickers, DropdownMenu, Popover, Tooltip and Toast appear and leave in place, while hover and focus color transitions keep --ca-duration-*. New derived tokens --ca-enter-fast, --ca-enter-base and --ca-enter-slow; MotionStage demos render their final state when the theme turns entrances off, as under prefers-reduced-motion (#268).

  • 16f89dd: Popover is controllable. bind:open and onOpenChange read and set the open state, and the children snippet receives close, which closes the popover and returns focus to the trigger, so a choice made inside it (a Calendar day, an option) can dismiss it. Existing popovers are unchanged.

  • 8c24c31: New --ca-radius-round token (999px × --ca-radius-scale): "fully rounded at the current scale". SwatchGroup swatches, the AppShell active pips (.ca-nav-item__pip, .ca-rail__pip) and the new-item dot, and .ca-nav-item__badge use it, so a theme with radius: 0 squares them with no app CSS (#267). --ca-radius-chip now aliases it. --ca-radius-pill stays a fixed 999px for functional shapes (Switch, Slider, Progress, scrollbars) and for controlRadius: "pill".

  • 946d69d: Tabs gains variant="line" (bare text tabs with an underline on the active one, for page headers), and TabsList and TabsTrigger forward rest attributes: aria-label names the tablist, data-*, id and event handlers reach the trigger button (#286). A TabsTrigger now points aria-controls at its panel only while a TabsContent with the same value is mounted, so tabs used as view switchers no longer reference missing elements.

  • 2ab0d93: ToggleGroup gets required: pressing the active item no longer clears the selection (radio-like, onchange stays silent; type="multiple" keeps its last pressed item). An empty group still selects on the first press. Closes the gap with SegmentedControl for filters that must look like contiguous buttons.

Patch Changes

  • 05cfbfc: AppShellLayout: an item at path: "/" is active on / only, no longer on every page, and a module at basePath: "/" only gets the pages no other module claims. Module prefixes (basePath, pathPrefixes) match whole path segments and the longest one wins, whatever the module order (/settingsfoo no longer matches /settings).
  • 98d2ace: Astro: the theme now survives <ClientRouter /> (View Transitions) navigations. caUI registers an astro:before-swap handler that carries #ca-theme, #ca-runtime-tokens, the theme-color meta and the theme attributes onto the incoming document (#265). codeatlas-ui init on Astro also adds svelte() from @astrojs/svelte to integrations, and notes npx astro add svelte when the dependency is missing (#273).
  • 3ae1f41: Button with href now types the anchor's own attributes: download, rel, hreflang, ping, referrerpolicy and type (the linked resource's MIME type) are accepted instead of rejected, as ...rest was already spread on the <a>. The props are a union keyed on href: a link takes anchor attributes, a button takes button attributes (form, formaction, name, value...), and <Button download> without href is a type error. At run time a type passed with href reaches the <a>, and a caller rel is kept and merged with noopener noreferrer when target="_blank" (it used to be overwritten).
  • 5c9ac81: CommandPalette: new shortcut prop (default true). With two palettes on one page, every instance opened on Mod+K; set shortcut={false} on all but one, or when the app binds its own shortcut.
  • a61874b: Calendar, RangeCalendar, DatePicker and DateRangePicker: fast arrow-key sequences no longer lose keys. The roving focus now moves synchronously instead of after tick(), so the next key always starts from the day it lands on, with or without Svelte's experimental.async.
  • e983caa: DropdownMenuItem forwards its remaining attributes (data-*, id, aria-*, title, ...) to the menuitem element. data-testid and aria-keyshortcuts used to be dropped. role, tabindex and the selection handler stay owned by the item.
  • ff8686f: DropdownMenuItem and TabsTrigger icon slots now size any svg or img inside the 14px box, not only a direct child. A SchemeAsset logo (its variants sit in display: contents spans) used to collapse to 0×0 when the svg had no width/height, and an svg with a fixed width="24" overflowed the box; both now fill the box.
  • d01c039: LoadingBoundary counts minDuration from the moment the placeholder reaches the DOM instead of from the state change that schedules it, so a busy main thread no longer shortens the time the placeholder is visible.
  • 23276cf: Fix Escape going to the wrong layer. A Dialog, AlertDialog, Drawer, Sheet, Lightbox, CommandPalette or DocsSearch that is playing its exit animation no longer takes Escape or presses: they reach the layer below, so closing a confirm dialog and pressing Escape right after closes the Drawer under it (#288). A menu or popover that was open when a modal opened now ranks above that modal, so Escape closes the menu first and the dialog second instead of closing the dialog and leaving the menu open (#299). Exit animations of components that use createPresence (Dialog, Drawer, AlertDialog, menus, popovers) now keep the node mounted through the exit instead of removing it in the flush that closes it, and a menu hosted in a dialog is no longer left stranded after its exit.
  • 05cfbfc: Rail: the dock magnification scales the module icon (the glyph), not the whole button, so the hovered background no longer covers the active neighbour's background and pip. The effect follows --ca-motion-scale: it is off with motion: { scale: 0 } and under prefers-reduced-motion.
  • 05cfbfc: Native button, input, select and textarea inherit the page font (font: inherit in the new ca.reset layer, styles/reset.css) instead of the browser's UA font. The AppShell section header (.ca-section__trigger / .ca-section__label) and any control without its own font rule now render in --ca-font-sans. Component and app rules still win: the layer is the first one.
  • 8c24c31: Skeleton and the skeleton prop follow the theme surface style: under flat, solid and outlined no gradient band is drawn, the placeholder is a flat block that pulses its opacity (ca-skeleton-pulse, stopped under reduced motion). Glass, frosted, neumorphic, clay, chrome and tonal keep the shimmer sweep (#280).

4.3.0 latest

npmGitHub release

Minor Changes

  • a1c0d04: Demo sidecars accept names (motion vocabulary) and motion (exact property, duration, easing, delay, stagger, spring). They are printed in e/NNN.txt and p/NNN.txt, carried in catalog.json and catalog.tsv, and ca-ui-mcp search_demos matches names and aliases before summary and tags.
  • a1c0d04: MotionStage and the MotionCtx type: the frame of a motion demo. It autoplays only while at least half of it is in view and the tab is visible, hands control to the user on pointer, key or focus (autoplay stops, Replay appears), always shows a Pause button with aria-pressed, and under prefers-reduced-motion never autoplays and tells the demo to render its final state (ctx.reduced). ctx.cycle increments each loop so a demo can re-run its animation. Labels come from the i18n catalog (motionStage.pause, motionStage.replay) and can be overridden with pauseLabel / replayLabel.

4.2.0 latest

npmGitHub release

Minor Changes

  • b65d56d: AI feedback loop. The ca-ui-mcp server has a new report_issue tool: an agent that hits a library problem (bug, missing prop or component, migration gap, wrong docs) gets a sanitized issue draft (paths, foreign URLs, emails, IPs, tokens, env assignments and foreign package scopes are redacted, and the redactions are listed) with a prefilled link to the new "AI report" issue form and a gh issue create command. It never posts. bunx @codeatlasdev/ui workarounds lists the // ca-ui-workaround(#<n>): <why> markers an agent left in your code and which issues are already closed (read-only, offline-safe, exit 0).
  • 81910d1: Avatar renders the photo as a real <img> (no detached Image() probe, so no double download) and passes loading, referrerpolicy and crossorigin through, so loading="lazy" keeps a 200-row list from firing 200 requests and referrerpolicy="no-referrer" works with CDNs that reject a cross-origin Referer. fallback now also takes a snippet (an icon for groups, channels and bots), and onerror fires when the image fails so you can refetch a signed URL. The fallback is always rendered beneath the image, so the box never changes size. The data-status attribute on .ca-avatar is gone.
  • 80c0336: Card has a padding prop ('none' | 'sm' | 'md' | 'lg', any CSS length, or a number of pixels) so a one-off density no longer needs an inner wrapper; flush stays as the alias of 'none'. The docs now say that Card's content is already padded, and in development a [ca-ui] warning (card-double-padding) fires once when the first child adds its own padding on top of the card's (the 20px + 20px = 40px "oversized card").
  • cd655bb: Chat primitives: MessageBubble (direction, grouped tails, sender and avatar slot, quote, reactions, delivery ticks), ConversationItem (inbox row with unread, pinned, muted, typing, active), AudioPlayer (lazy src, waveform from peaks, slider semantics, playback rate), MediaGrid (image, video and document tiles opening a Lightbox gallery) and VirtualList (windowing plus load-older at the top that keeps the scroll position). New chat, audioPlayer, mediaGrid and virtualList messages in en and pt-BR.
  • 80c0336: CopyButton takes aria-label for the icon-only form (label=""), so each row of a list or table can announce its own name ("Copy client_id of Acme") instead of the same localized "Copy". The default stays the i18n copyButton.label. The label is ignored when label is visible, so the accessible name keeps containing the visible text. Remaining attributes (data-*, id, aria-describedby, …) are now forwarded to the underlying button.
  • 6e08799: Every numbered demo now has a reviewed <Variant>.meta.json sidecar (summary, use, spec, tags, shape), so get_demo, search_demos and /api/v1/en/{e,p}/NNN.txt return what the demo shows, when to use it and a Spec list of its literal values (props, snippets, state, counts). The catalog check (manifest --check, run by every build) now fails on a demo without a sidecar, a sidecar without a demo, an unknown key, a bad shape, or a spec line that quotes a token that is not in the demo source. New codeatlas-ui pick [ids...] opens the gallery, listens on 127.0.0.1 with a random per-run token and prints the ids you send with "Send to AI" (#005 #012).
  • d99cead: codeatlas-ui init scaffolds SEO and PWA on SvelteKit (#205): prerendered manifest.webmanifest, robots.txt and sitemap.xml routes built with the seo helpers, a service worker (src/service-worker/index.ts on $app/service-worker, $app/manifest and $app/env for Kit 3; src/service-worker.ts on $service-worker for Kit 2) with a prerendered /offline page, and a default <Seo> with canonical and manifest link in the root layout. Existing files are never overwritten; after creating them init reminds the app to set paths.origin (otherwise Kit's prerender placeholder origin reaches the sitemap and canonical URLs), add the icons and exclude src/service-worker from tsconfig.json on Kit 3.
  • d99cead: New InstallPrompt (#208): a dismissible banner that offers to install the app. On Chromium it keeps beforeinstallprompt (no browser mini-infobar) and shows an Install button that opens the native dialog and reports the choice with onchoice; on iOS Safari and iPadOS, which have no install API, it shows the Share, Add to Home Screen steps. It renders nothing when the app already runs standalone, and stays hidden for dismissDays (default 14) after the user closes it. Strings are in the installPrompt i18n group (en, pt-BR); the root: Button only size budget rises by 240 B gzip because both catalogs ship with every root import.
  • 81910d1: Lightbox opens videos and galleries. items ({ type?: 'image' | 'video', src, alt?, caption?, meta?, poster?, download? }, exported as LightboxItem) plus a bindable index add previous/next buttons, ArrowLeft/ArrowRight and swipe (no wrap; the end buttons are aria-disabled so focus stays put), a counter, caption and sender/time line, video with controls (paused when you move on), and a Download button (ondownload or a plain <a download>). src/alt still work as a single-item shortcut. New lightbox.previous, next, download and counter labels in en and pt-BR; the close button moved into a .ca-lightbox__tools bar.
  • 3180023: Leading icon slot for DropdownMenuItem and TabsTrigger, and a documented pattern for brand logos (#194). icon is a snippet rendered in a fixed 14px box with the standard gap and aria-hidden (the label stays the accessible name), so icon size and alignment no longer drift between menus. brandIcon marks the icon as a fixed-color brand mark: no highlighted, disabled, active or destructive color reaches it (trademark guidelines forbid recoloring). New SchemeAsset takes light and dark snippets and shows the one for the kit's color scheme (.dark on <html> or a data-theme-scope island) with CSS only, so it is right in SSR and on first paint and apps no longer read the theme store for logo variants. New classes: .ca-dropdown-item__icon, .ca-tabs-trigger__icon, .ca-scheme-asset, .ca-scheme-asset__light, .ca-scheme-asset__dark.
  • 7a112eb: Navigation: LoadingBoundary wraps <svelte:boundary pending> with a 200 ms delay and 500 ms minimum so a loading UI never flashes (default placeholder is Skeleton rows, aria-busy, named region). @codeatlasdev/ui/kit gains NavigationProgress (top bar driven by navigating from $app/state) and the pendingLink() attachment that sets data-pending on the link being navigated to. A test now proves AppShell stays mounted across 10 client-side navigations.
  • df50d86: New RecordList, RecordRow and RecordCell: records as dense rows (identity, a few columns, a row menu) that re-flow to a compact two-line row on a phone. The list owns the flush container, the separators, one columns template shared by the header and every row, the re-flow (a container query on the list's own width, 36rem), the 44px actions cell and an aria-hidden header whose labels become per-cell dt when stacked. It replaces the hand-rolled flush Card + grid + :global row CSS, and is much shorter on a phone than Table responsive="cards".
  • d99cead: <Seo> completes the social and language tags (#207): og.defaultImage is the site-wide card used when a page has no og.image (also feeds twitter:image and the summary_large_image card), twitter:title and twitter:description mirror the page title and description, and the new lang prop sets og:locale and keeps <html lang> in sync on the client.
  • 81910d1: skeleton prop on Avatar, KpiCard, TextField, Table, Card and GlassCard: the component keeps its real DOM, so the placeholder has exactly the loaded size (no layout shift). Content is laid out but hidden, the host paints the same shimmer as <Skeleton>, and it is announced with aria-busy="true"; a skeleton TextField is disabled and an Avatar makes no image request. Reduced motion stops the sweep and forced colors draw an outline. New global [data-skeleton] rule and ca-shimmer keyframes in utilities.css.
  • f889334: Every backdrop blur now comes from one place. Components no longer declare backdrop-filter: they set data-ca-blur="<role>" (or are a .ca-surface) and styles/surface.css paints it from the tokens <html data-style> rewrites, so one attribute turns every blur off or down together. The look is unchanged (guarded by a computed-style test over 9 styles, light and dark). FloatingLayer gains a blur prop (overlay or tooltip). lint:tokens rejects backdrop-filter outside surface.css and materials.css. The MCP server gains list_surfaces, generated from the [data-style] selectors in materials.css, and dist/surfaces.json ships with the package.
  • 3180023: TabsList handles its own overflow (#190). It keeps the width of its tabs inside Tabs (no more full-column pill on desktop), and when the tabs do not fit it scrolls sideways on its own with the scrollbar hidden and scroll snapping, instead of wrapping or widening the page on a phone. The selected tab is scrolled into view on mount and when it changes (only the strip scrolls, never the page). New optional fade prop masks the edge that still hides tabs, like the AppSettings nav. The wrapper div with overflow-x: auto and the :global(.ca-tabs-list) { width: max-content } workaround can be deleted.

Patch Changes

  • 526fee2: A menu, Select list, popover or tooltip that is already open when a Dialog, Sheet or Drawer opens (a shortcut or an async action opening the modal over it) now moves into that modal and stays clickable and above it. Before, it stayed outside the modal, so it went inert and could not be used. When a modal closes before the popovers hosted in it, they are handed to the next modal or put back where they were declared instead of being removed with it. The layer stack and the stacking contract are covered by new tests and documented on the Dialog page (#225).
  • 3afd417: Overlays return focus to the control that opened them on iPhone/iPad and Safari too. WebKit does not focus a tapped button (it focuses a focusable ancestor such as <main tabindex="-1">, or nothing), so focus used to land there on close and VoiceOver users lost their place. CommandPalette: Mod+K focuses the search field on touch devices with a keyboard (covered by a test now).
  • c76c837: New guide getting-started/migrate-react, readable offline through the MCP get_guide("migrate-react") and on the docs site: decision flow (incremental or rewrite), React hook to Svelte 5 rune map with the effect pitfalls, MUI / shadcn-Radix / Chakra to @codeatlasdev/ui component maps, layout templates, theme mapping, TanStack Table to @codeatlasdev/ui/table, and how to report a gap (report_issue kind migration-gap plus a ca-ui-workaround marker). A unit test fails when a component named in the maps stops being an export.
  • d99cead: The manifest generateBrand / caBrandPlugin write is installable: display: "standalone" (was "browser"), id and scope set to "/", and theme_color / background_color taken from the theme's default scheme instead of always the light one (#206). The icon hash now includes the default scheme, so switching it renames the files.
  • 81910d1: Skeleton's shimmer is a translated ::after band instead of an animated background-position, so it no longer repaints on every frame. Reduced motion still gets a static, dimmed placeholder.
  • 9c9df3b: @codeatlasdev/ui/vite is safe in the SSR graph: importing designStyle, mergeTheme or ThemeOptions from a theme.ts that hooks.server.ts shares no longer fails the SvelteKit server build with UNLOADABLE_DEPENDENCY on @resvg/resvg-js (#184). The native peers are loaded by name at run time and caBrandPlugin marks them ssr.external, so the ssr.external workaround is no longer needed. runBrand and brandUrls are now exported from @codeatlasdev/ui/vite, as the 4.0.0 changelog announced (#185).

4.1.0 latest

npmGitHub release

Minor Changes

  • ccae97c: Numbered demo catalog (ca-prompt): every docs demo has a stable #NNN id. The package ships demos.json (export @codeatlasdev/ui/demos.json) and dist/api/v1 reference texts, and ca-ui-mcp gains search_demos and get_demo. The agent skill documents picking demos by number.

Patch Changes

  • 6ab52ee: Checkbox, Switch and RadioGroup: on touch screens (pointer: coarse) a tap on the box toggles the control again. The 44px hit-area overlay painted over the native input and swallowed the tap; the native input now is the 44px hit area.

4.0.0 latest

npmGitHub release

Major Changes

  • b99075a: AppShellLayout renders its content region as <main id={mainId} tabindex="-1"> by default, so a page built on it has a <main> landmark (axe landmark-one-main, region) and the skip link lands on it. It was a plain div. Pass mainId={null} when your page renders its own <main id="main" tabindex="-1">: the region is then a plain div, never a second landmark. If you wrapped your content in <main> inside the shell, remove it or pass mainId={null}.
  • fdb6798: nativeFeel() now has one opt-out class, .ca-selectable, for both the context menu and text selection (#136). The context-menu filter used .selectable while the selection CSS used .ca-selectable, so neither class restored both. The filter and its docs now say .ca-selectable, matching the ca-* class contract. Clean cutover: .selectable was never documented outside a JSDoc line, and no stylesheet ever targeted it, so apps using it must rename it to ca-selectable (right-click on a .selectable region is now blocked again until they do).

Minor Changes

  • 6e39edc: caBrandPlugin writes src/generated/brand.ts as soon as the Vite config resolves (the URLs depend only on a hash of the inputs, so no rendering is needed), not only at build start. New codeatlas-ui brand command and brandUrls() / runBrand(): render the icons and the module without a Vite run, so a job that only runs svelte-check on a fresh clone type-checks (svelte-kit sync does not load Vite plugins).
  • c2d255d: FormSection accepts headingLevel (2 | 3 | 4 | 5 | 6) and AppSettings/AppForm provide the section heading level like PageShell, so FormSection and EmptyState titles inside them render as <h2> under the page <h1> (axe heading-order) instead of <h5>. Outside a page layout the default stays <h5>. Only the tag changes, never the look.
  • af02ce2: RadioGroup, Checkbox, Switch and Select accept name, so their value is part of FormData on a native form submit (progressive enhancement, SvelteKit form actions without a hidden-input workaround). Select renders a hidden input carrying the selected value when name is set.
  • e7b85e7: ogCard accepts lang (BCP 47, default "en", escaped) for the card's <html lang>, so non-English cards get the right hyphenation and locale font selection.
  • 55ecc1a: Box, Flex, Stack and Grid are generic on as: <Stack as="form" novalidate onsubmit={...}> accepts the form's attributes, and the handler's currentTarget is an HTMLFormElement (same for as="a", as="label", as="button"...). An attribute that does not belong to the tag is a type error. Exports PolymorphicProps and ElementTag.

Patch Changes

  • 153be87: centsToDisplay keeps the no-break space Intl puts after the currency symbol ("R$\u00a0123,45"), so a price no longer wraps between the symbol and the number. MoneyInput still shows and parses a plain space in its editable text.
  • a517102: toCsv, toJson, ExportColumn and ExportMenu accept any object row type (T extends object), so rows typed with an interface no longer need a cast. key is still checked against keyof T.
  • 8d25c2f: caErrorPlugin({ scaffold: true }) writes a valid src/error.html: the theme init script now sits in <head> instead of inside <style>, the page uses the app theme (theme option, same object as caThemePlugin) instead of hard-coded amber, its copy and lang follow the new locale option (en default, warns when unset), and the javascript:history.back() link is gone (CSP-safe). The scaffolded +error.svelte uses tabs and double quotes like the other generated files. init now writes caErrorPlugin({ scaffold: true, theme }).
  • 9e5655f: init without --write is now a true dry run: it prints would create <file> instead of writing new files. It also stops creating src/lib/theme.ts (src/theme.ts on Astro) when vite.config.ts / hooks.server.ts already import theme from elsewhere, and wires the files it adds to that import.
  • a7f7e96: Docs and agent skill: apps must install @lucide/svelte themselves. Under Bun's isolated linker the package's own dependency is not importable from the app.
  • 3ef5b11: migrate keeps the default thickness of GlassCard and GlassSurface: a usage without thickness (default regular) becomes elevation={3} instead of dropping it, so the migrated card keeps its fill, blur, shadow and rim (Card and Surface default to elevation={1}).
  • 33ad505: migrate looks up typescript in the project directory first (then next to the CLI) and prints migrate needs typescript (bun add -d typescript) with exit code 1 when it is missing, instead of crashing with Cannot find package 'typescript'. This also fixes bunx @codeatlasdev/ui migrate in layouts where the CLI cache directory cannot see the project's typescript.
  • b7cdd79: migrate now ends with a list of the usages it left for you (file:line Component: reason (recipe)), and --strict exits with code 1 when any remain. A Grid or Stack whose cols/align could not be mapped still gets its gap converted: gap={20} next to cols="2fr 1fr" becomes gap="5" instead of silently turning into step 20 (80px). Dynamic or off-scale gaps are reported, since their meaning changed from pixels to a step.
  • 98ea2ed: Nested modals: only the newest focus trap owns focus, so opening a Dialog/AlertDialog/Sheet/Drawer over another no longer loops on focusin (Maximum call stack size exceeded); closing it hands focus back to the one below.
  • daccdc8: .ca-prose heading anchors stay visible on touch screens: a stray second @media (hover: none) block hid them with display: none, contradicting the rule above it. New "Release lists" docs page documents the ca-release* markup and the version-id anchor convention.
  • c0bc624: --ca-scrim-blur now drives the modal scrim (#135). It was declared as none but never read: the ::backdrop of Dialog, AlertDialog, Sheet, Drawer, CommandPalette and DocsSearch hardcoded blur(4px) saturate(180%). The scrim reads the token and the token defaults to that same value, so nothing changes visually; :root { --ca-scrim-blur: none } now removes the blur from all of them. The CommandPalette scrim shares it (it used a plain blur(4px), now with the shared saturate(180%)).
  • 5a3ede3: Agent skill and AGENTS.md import ThemeOptions from @codeatlasdev/ui/vite (/astro in Astro), like the docs and init. The package root pulls .svelte files that Node cannot load from vite.config.
  • 8185835: New --ca-tooltip-blur token for the Tooltip surface blur (#137). It defaults to var(--ca-blur-ultra-thin), so the look is unchanged; --ca-tooltip-blur: none gives an opaque tooltip without re-tiering the shared ultra-thin blur (.glass-ultra-thin, .glass-btn-clear, --ca-upload-blur).
  • 3dda885: @codeatlasdev/ui/vite exports designStyle, DESIGN_STYLES, mergeTheme and DesignStyleId, so a theme.ts shared with vite.config can use a design recipe (including the 3.0 migration designStyle("liquid", { ... })) in plain Node. The theming guide and the init theme comment now point to this entry instead of /astro.

3.1.0 latest

npmGitHub release

Minor Changes

  • 0579bb3: .ca-prose styles release lists, so a changelog page needs no custom CSS (#126). Wrap one <section class="ca-release"> per version (heading with its channel .ca-chip, then a .ca-release__meta row of date and links, then the rendered notes), add a .ca-release-jump pill index and close with .ca-release-thanks. Anchored releases clear a sticky DocsShell header (scroll-margin-top), and the meta links and pills keep 44px touch targets. Classes only, no JS or component: the markup can come from Markdown rendered at build time or at the edge.
  • 973fe00: Add SwatchGroup: a single-choice color swatch picker (role="radiogroup", roving focus, preset or custom color). Its coarse-pointer hit area is the swatch itself, so neighbouring swatches never overlap and every click lands on the swatch the user aimed at.

Patch Changes

  • b27c73a: generateBrand and the brand Vite plugin no longer load every system font for each PNG they render. resvg did that by default on every construction (~450 ms per render on Windows, ~2.7 s per icon set, at every build and dev start); fonts now load only when the mark has <text> (#120).
  • 5c79b24: FilterGroup draws its label with --ca-text-secondary instead of --ca-text-hint. The hint role can be set low by a custom preset, and the label failed the 4.5:1 axe check on a raised surface in dark mode.
  • 5c79b24: KpiCard is built on Card instead of the deprecated GlassCard, so rendering it no longer logs the [ca-ui] deprecation warning in dev. The look is unchanged (elevation 3).

3.0.0 latest

npmGitHub release

Major Changes

  • a1d14f5: Sober default theme, Liquid Glass is now opt-in (#111). The package used to look like a UI-kit demo: a glass material over a three-hue mesh, glow on selected states, a white rim on every surface, hue-tinted 24-40px shadows, 10px controls, 2px icons and 700 headings. The default is now solid and neutral:

    • defaultStyle: "solid", paper: "plain", radius: 0.8 (--ca-radius-md is 8px), iconStroke: 1.75, heading weight 600 (and no extra h1 tracking).
    • Surfaces, text and outlines carry almost no chroma (the brand hue stays on fills, focus, links and charts); shadows are neutral black (--ca-shadow-*, --ca-glass-shadow, --ca-mat-*-shadow).
    • [data-style="solid"] and [data-style="flat"] set --ca-glow-primary(-strong) and --ca-glass-rim to transparent and draw --ca-mat-edge as a hairline, so .glass-tinted, .glass-prominent, .glass-glow, .glass-active, .glass-btn-prominent and the AppShell nav pills lose their halo and rim without a component change. The page mesh (--ca-page-mesh) is drawn only by glass and frosted.
    • New recipe designStyle("liquid") reproduces the former look exactly (glass, mesh, glow, round corners, bold headings, tinted shadows). designStyle("default") now means the sober default.

    Migration: to keep the old look, use designStyle("liquid", { ...yourTheme }) (import it from @codeatlasdev/ui/vite in a theme shared with vite.config: the root and /kit cannot load in Node), or set defaultStyle: "glass", paper: "mesh", radius: 1, iconStroke: 2 and typography: { weights: { heading: 700 } }. colorScheme.setStyle("glass") still switches at runtime. Apps that override --ca-* tokens are unaffected; apps that read --ca-radius-* or the shadow tokens get the new values. defineTheme() with no options now resolves the new defaults (designTokens emits nothing for them).

Minor Changes

  • 8b36a1e: AppShellLayout: the shell keeps its state across page loads (#109). On an MPA (Astro, a full reload) every navigation is a new document, and the panel, the selected module, the expanded sections and the panel scroll all reset. persistPanel (default true) now keeps the panel open/closed, the selected module and the section open map in localStorage["ca-shell"] (the old ca-panel-open key is migrated), and the panel scroll per module in sessionStorage. An inline script emitted right after the sidebar restores them before the first paint and sets data-ca-panel / data-ca-module on <html>, so a stored-closed panel is laid out closed with no flash, on the same model as the theme script. Stored values are validated and discarded when malformed. The user's last choice for a section wins over defaultOpen; the section holding the current page always opens. The shell now starts at its target width instead of animating open on load. persistPanel={false} keeps the old always-default behavior for demos and embeds.

  • 3476d9a: Brand assets from one mark. caBrandPlugin and generateBrand (@codeatlasdev/ui/vite) render a content-hashed favicon (SVG that follows prefers-color-scheme, ICO, PNG), apple-touch-icon, 192/512/maskable manifest icons and a web app manifest from one SVG with {{ink}}/{{spark}} placeholders and your theme; resolveBrand and renderMark expose the resolved sRGB colors. iconLinks and ogCard (@codeatlasdev/ui/seo) emit the head links and the 1200x630 social-card HTML for renderOgImage. <Seo> now also writes og:image:type and twitter:image. @resvg/resvg-js and png-to-ico are optional peer dependencies.

  • 1eeca1c: Surface engine and layout primitives (#112).

    New components: Box, Flex, Stack, Grid, Container, Section (layout), Surface, Card and SurfacePanel.

    • Layout primitives take token props (gap, p/px/py, align, justify, direction, columns, wrap) and a polymorphic as. A prop renders as a data-* attribute (data-gap="4"), a responsive object ({ base: "2", md: "4" }) as data-md-gap="4", styled by styles/layout.css in @layer ca.utilities. No inline style.
    • Surface has orthogonal axes: elevation 0-5, material auto | solid | frosted | glass | liquid, tone, variant filled | outlined | ghost, radius, interactive, glow, as. One .ca-surface rule reads --ca-surface-* variables. material="auto" follows <html data-style>; the explicit materials look the same under every style. It never sets will-change or isolation, and it has forced-colors and reduced-transparency entries.
    • Card (title, headingLevel, href, flush, full, headerActions) is built on Surface; SurfacePanel is a raised <section> (the AppShell sidebar Panel keeps its name).
    • Docs: Surface, Card, Layout primitives; docs/css-api.md lists the new ca-* classes and the data attributes.

    Behavior changes to review in Stack, Grid and Row (they were the only layout components with inline style):

    • Stack and Grid gap is now a spacing step string (gap="4" is 16px), not pixels. align / justify use start | center | end | between | ... instead of raw CSS values. Grid cols is now columns (1-12, or a responsive object); rowGap, colGap and CSS template strings ("2fr 1fr", repeat(auto-fill, ...)) are gone: write grid-template-columns in a class.
    • Row is deprecated in favor of Flex. It still renders .ca-row and maps a pixel gap to the matching step.
    • GlassCard and GlassSurface are deprecated in favor of Card and Surface, and removed in the next major. They render the new components, keep their ca-glass-card* and glass* classes and look the same under every data-style, and log a [ca-ui] warning in development.

    Size: styles/core.css grows from 9.6 to 13.0 kB gzip (limit 9.6 to 13.6 kB), global.css from 12.6 to 15.9 kB: layout.css is ~2.2 kB (every token prop and step at the base and four breakpoints, generated by scripts/build-layout.ts) and surface.css ~1.2 kB (the five materials by six elevations). JS entries are unchanged.

    Migration: bunx @codeatlasdev/ui migrate --write src rewrites GlassCard to Card material="glass" (thickness to elevation, tinted to tone="brand", glowing to glow, static padding to p), GlassSurface to Surface (refraction to material="liquid"), Row to Flex, and Stack/Grid pixel gaps, cols and align/justify values. Dynamic or off-scale usages are left as they are, with their import, for you to finish.

  • 49b2bf4: Unified layer engine. Dialog, AlertDialog, Sheet, Drawer, Lightbox, CommandPalette, DocsSearch, DropdownMenu, Popover, Tooltip, Select, Combobox, NotificationBell and the date pickers now share one layer stack (primitives/layer.ts).

    • Modals are native <dialog> + showModal(); menus, popovers and tooltips are popover elements. Both live in the top layer, so stacking no longer depends on z-index: a menu inside a dialog is always on top. The --ca-z-dropdown and --ca-z-tooltip tokens remain for your own in-flow UI but are no longer used by overlays.
    • Anchored layers use CSS anchor positioning when supported and Floating UI otherwise.
    • One Escape / outside-press / focus stack: only the topmost layer reacts, so nested overlays dismiss in order. Focus returns to the trigger on close.
    • New onLayerChange(listener) (and a layerchange CustomEvent on document) fires on every layer open and close with the open layers and their rectangles, so hosts can park or snapshot native views.
    • The .ca-floating wrapper div is gone: .ca-floating is now on the layer element itself (the one carrying data-state/data-side), and .ca-date__float was removed.

2.3.2 latest

npmGitHub release

Patch Changes

  • 9b7015f: DocsShell: the sidebar keeps its place between pages. On a static docs site every sidebar link loads a new document, and the sidebar used to jump back to the top, hiding the page just picked (#99). The scroll position is now saved per section in sessionStorage (on a sidebar click and on pagehide) and restored by an inline script emitted right after the sidebar, before the first paint; the current page is scrolled into the sidebar's view when it is outside it. Only the sidebar scrolls: the window and focus are untouched. Client-only mounts and pages whose CSP blocks inline scripts get the same restore on hydration.

2.3.1 latest

npmGitHub release

Patch Changes

  • 3280fc2: Theme islands: a data-theme-scope inside a data-preset island now takes that island's preset instead of the page's. Before, the page preset's rule also matched the scope, so a brand preset on <html> (unlayered) always won, and between two built-ins the one emitted later in global.css won. The landing hero demo ignored its preset swatches because of this (#87).

    Bundle budget: styles/global.css +122 B gzip (limit raised from 12800 to 12900 B). Each preset's scope selectors (light, dark and their prefers-contrast: more copies) now carry the nearer-island exclusion; core.css stays within its limit.

2.3.0 latest

npmGitHub release

Minor Changes

  • 1f858d3: The generated API reference now documents every non-component export of every entry point (root, kit, vite, astro, actions, seo, forms, tokens, table, charts): setLocale, registerLocale, caErrorPlugin, createErrorHandler, nativeFeel, usePageTitle, primeKeyboard, the stores, the seo builders and the types, each with its signature and a JSDoc summary. components.json gains an exports array, API.md gains a "Non-component exports" section, the ca-ui-mcp server gains a get_export tool, and components of kit such as RouteAnnouncer now appear in the component list. The summaries were added as JSDoc, so they also show in editor tooltips.

  • 648a1e6: New .ca-sr-only utility (visually hidden, still in the accessibility tree) and .ca-sr-only--focusable (shown again while it or a descendant has focus, for skip links). Apps can now add a table <caption> or an AT-only label without custom CSS. Both live in styles/utilities.css (ca.utilities layer), so core.css and global.css carry them. Documented in getting-started/accessibility and docs/css-api.md.

    The kit now uses it instead of eight copies of the clip pattern (Dialog, Select, DocsSearch, Lightbox, the date pickers, and the page titles of PageShell, AppForm, AppList and AppSettings; the Table cards-mode header row shares the same rule). Visual output is unchanged.

    Migration: four internal class names are gone because they only existed to hide text. If your CSS targeted .ca-dialog__sr-only, .ca-sel__sr, .ca-docs-search__sr or .ca-lightbox__sr, target .ca-sr-only instead (these elements now carry that class). The page <h1> hidden inside AppShellLayout below 900px now also carries .ca-sr-only--shell.

    Bundle budget: styles/core.css +173 B gzip and styles/global.css +206 B gzip (limits raised to the measured 9812 / 12800 B). The cost is the new public utility (the box, the focusable variant and two scoped copies that a class cannot express as one rule: the shell page title under a media query, the Table cards header under a container query); the eight component-level copies it replaces were already part of component CSS, not these entry files.

  • 19386b1: Add the @codeatlasdev/ui/testing subpath: expectNoA11yViolations, checkA11y, formatViolations, settled and A11Y_EXCEPTIONS. It is the library's own axe helper (animations settled, region disabled for isolated components), framework-agnostic (takes an Element or Document), with axe-core as an optional peer dependency.

  • a88b8a9: EmptyState and GlassCard accept headingLevel (2 | 3 | 4 | 5 | 6) to render their title as that heading without changing how it looks. EmptyState inside PageShell now defaults to <h2> (it was always <h5>, which made axe report heading-order under the page <h1>); outside a PageShell it stays <h5>. GlassCard renders title as a <p> unless headingLevel is set, so existing outlines are unchanged. New exported type HeadingLevel.

  • 5f7d842: New KpiGrid: a responsive row of KpiCards that never leaves an empty cell. Grid cols="repeat(auto-fill, minmax(160px, 1fr))" ignores how many cards it holds, so 3 cards on a 390px phone became 2 + 1 with a hole. KpiGrid counts its children in CSS (:has() quantity queries, no JavaScript, SSR-safe) and only uses column counts that divide that number: 3 cards are 3 across or one column, 4 are 4, then 2 x 2, then one column, 6 are 6, 3 + 3, 2 + 2 + 2. It follows its own width (container query), so it also works inside a sidebar layout. Above 12 children it falls back to auto-fill. Props: gap, class. New classes: ca-kpi-grid, ca-kpi-grid__grid.

    The docs, templates, demos and agent skill now use KpiGrid for KPI rows instead of the Grid auto-fill recipe.

  • 4db46ab: Three patterns for result views, from the DX feedback in #48. LiveList shows rows that stream in (SSE, WebSocket, bulk jobs): a named role="log" with batched polite announcements instead of one per row, determinate or indeterminate progress, and auto-scroll that follows the newest row only while the user is at the bottom, pauses when they scroll up (with a "N new" jump button) and never moves focus. ExternalLink is a link to another site with target="_blank", rel="noopener noreferrer", a decorative icon and a localised "(opens in a new tab)" for screen readers (externalLink.newTab in the i18n catalog), made for table cells. ExportMenu is an Export button with a CSV / JSON DropdownMenu; the helpers toCsv, toJson, downloadFile and the ExportColumn type are exported too (RFC 4180, UTF-8 BOM in the downloaded CSV, spreadsheet formulas neutralised). New i18n keys: externalLink, exportMenu, liveList (en, pt-BR).

  • 11809ff: MoneyInput accepts currency (ISO 4217; decimals come from Intl, so JPY has none and BHD has three) and locale (BCP 47). value is now the currency's minor unit rather than always cents. Existing BRL usage is unchanged: with neither prop set the field still renders R$ 1.234,56. With currency set and no locale, the locale follows the active i18n locale (I18nProvider / setLocale). centsToDisplay and parseInputToCents take an optional { currency, locale } (defaults BRL / pt-BR), parseInputToCents reads the locale's decimal separator, and currencyDecimals(currency) and the MoneyOptions type are exported from @codeatlasdev/ui/forms.

Patch Changes

  • 5871641: AppShellLayout: the panel/main offset now uses the Svelte 5 Spring class and lands on the exact target (252px, not 251.984px). The extra CSS margin-left transition that lagged the spring is gone, and prefers-reduced-motion skips the animation and jumps straight to the target.
  • ef7b0fa: CommandPalette: keyboard selection no longer gets stolen by the pointer. A row appearing under a stationary pointer (list renders, dialog animates in) fired mouseenter and moved the highlight; hover now follows real mousemove. ArrowDown on an empty list no longer stores -1.
  • ee47b59: DocsSearch: keyboard selection no longer gets stolen by the pointer or reset by a lazy index. A row appearing under a stationary pointer (results render, dialog animates in) fired mouseenter and moved the highlight; hover now follows real mousemove. A lazy index() resolving while the dialog is open re-ran the open effect and wiped the typed query. ArrowDown on an empty list no longer stores -1.
  • 7f5d8f7: init now writes the full SvelteKit setup: caErrorPlugin({ scaffold: true }) next to caThemePlugin(theme) in vite.config.ts, and handleError = createErrorHandler() next to the theme handle in src/hooks.server.ts (both patched into existing files with --write, each only when missing; your own handleError is never replaced). The installation guide shows one complete SvelteKit setup, the exact files init writes, and states that plugin order in plugins does not matter.
  • ea072c2: DropdownMenu, DataTable column toggle (and any FloatingLayer consumer): closing before the frame-delayed focus move ran no longer lets that move steal focus back from the trigger. FloatingLayer's onMounted may now return a cleanup that runs on close/unmount.
  • ba4c458: PageShell actions below 900px: a lone icon-only control (IconButton, ThemeToggle) no longer stretches to the full row with its icon centred. Icon-only children (.ca-icon-btn, .ca-rail-btn) keep their natural size and sit at the end of the row; labeled buttons still share the row full-width as before.

2.2.0 latest

npmGitHub release

Minor Changes

  • d9087e8: Accessible tokens and states, solved per preset and scheme by the generator (every built-in and brand preset, light and dark):

    • Disabled controls no longer fade with opacity. Button (all variants, including the new destructive and glass disabled looks), IconButton, Checkbox, Switch, Slider, Tabs, ToggleGroup, SegmentedControl, Pagination, PinInput, NumberField, RadioGroup, Select, Combobox, TextField, TextArea, date fields and the data-table controls draw the disabled state from --ca-text-disabled, --ca-action-disabled-bg and --ca-outline-default. A loading button keeps its enabled look.
    • --ca-text-disabled is now guaranteed >= 3:1 on every surface and on the disabled fill, and documented (comment, tokens.json description, docs) as the label of disabled controls only. lint:tokens flags color: var(--ca-text-disabled) outside a disabled selector.
    • New --ca-outline-control (>= 3:1 on every surface and on the 5% field tint, WCAG 1.4.11). --ca-field-border-color defaults to it, so TextField, TextArea, Select, Combobox, NumberField, PinInput and the date fields, plus the Checkbox and Radio boxes, reach 3:1. --ca-outline-subtle stays for dividers. Overridable like any --ca-* token.
    • New --ca-semantic-error-contrast, the pair of --ca-semantic-error-fill. Button variant="destructive" reads it instead of a literal white. defineTheme derives it from a custom colors.*.error.
    • Menus, popovers, pickers and panels are near-opaque by default (--ca-overlay-bg / --ca-panel-bg are 94% of --ca-surface-overlay, the blur is kept), so label contrast no longer depends on the page behind. The old tints are --ca-overlay-bg-glass and --ca-panel-bg-glass. The frosted style keeps the opaque default.
    • --ca-focus-ring is solved per preset and scheme (>= 3:1 on every surface and on the hover/selected fills) and every focusable component reads it (it was a literal --ca-brand-main in about 20 components; the date fields now draw a real 2px ring instead of a 40% tint). New --ca-state-indicator (same guarantee) for the switch track, slider range and segmented thumb edge. lint:tokens rejects a literal --ca-brand-main in a focus rule.
    • prefers-contrast: more and forced-colors keep working: the contrast block also lifts --ca-outline-control and --ca-text-disabled.
    • tokens.json entries now carry a description taken from the comment above the declaration.

    Migration: apps that doubled the class to patch these (.ca-btn.ca-btn:disabled, .ca-tf__wrap.ca-tf__wrap, .glass-overlay.glass-overlay, .ca-btn--destructive.ca-btn--destructive, a global :focus-visible redraw) can delete those overrides.

    Bundle budget: styles/core.css +61 B gzip and styles/global.css +277 B gzip (limits raised to the measured 9639 / 12594 B). The cost is the three solved tokens (--ca-focus-ring, --ca-outline-control, --ca-semantic-error-contrast) and the two prefers-contrast: more lifts in 16 preset blocks, which carry the guarantees above.

  • d9087e8: Component CSS now lives in @layer ca.components, declared last in the kit's layer order, so the documented contract is true: an unlayered app rule with the same single-class selector (.ca-btn:disabled, .ca-tf__wrap, .ca-tooltip-content, .ca-dialog__body) overrides the kit, with no doubled classes and no !important. Every component <style> block, Dialog/dialog.css and Date/date.css are wrapped, and each starts with the full @layer order statement so bundler chunk order cannot demote the layer. The order in core.css changed to ca.reset, ca.tokens, ca.themes, ca.base, ca.materials, ca.utilities, ca.components. The redundant !important on the error state of TextField, TextArea, NumberField and Combobox was removed (it would have beaten app CSS now that it sits in a layer). Behavior change: app rules that previously lost to a kit rule of equal or higher specificity now win; apps can delete their .ca-btn.ca-btn / !important workarounds. Layered app CSS (for example Tailwind @layer components) must be declared after the kit order statement to outrank it.

  • d9087e8: i18n: open locales and request-scoped language (fixes #46).

    • Locale is now string (BCP 47). registerLocale(id, messages) adds any language with a partial dictionary; missing keys fall back to English, and region tags resolve to the base language (es-MX → es). getLocales() and getMessages(locale, override?) are exported.
    • New <I18nProvider locale messages?>: the language lives in Svelte context, so concurrent SSR requests never share it, and it stays reactive to prop changes. useLocale() and useT() read it from components.
    • setLocale is now a no-op on the server (with a dev warning), because the module state it wrote was shared across every concurrent request. SSR apps must use I18nProvider; client-only apps keep setLocale. A provider always wins over setLocale.
    • createErrorHandler no longer reads global locale state. It takes locale (a tag or (event) => tag) for the fallback message.

    Migration:

    • SSR (SvelteKit) apps: replace setLocale(x) in the root layout with <I18nProvider locale={x}>…</I18nProvider>. Before, the server-rendered HTML used the locale of whichever request called setLocale last.
    • Custom language via setLocale("en", fullOverride): call registerLocale("es", dictionary) and use locale="es" / setLocale("es"). Date and number formatting can now follow the real locale.
    • createErrorHandler(): add locale if the fallback message must not be English.
    • Removed export: the internal global t() is gone (it was never exported from the package root). Library code uses const t = useT() at component init.

Patch Changes

  • d9087e8: Fix the component descriptions in components.json, the ca-ui-mcp server and the skill's API.md: the manifest no longer takes the first JSDoc found in a component (usually a prop's, e.g. TextField read @deprecated Use description instead). Only a detached component-level doc comment counts, and tag-only comments are skipped.

    bunx @codeatlasdev/ui init now supports SvelteKit 3: it reads the installed Kit version, never writes the removed $lib import (uses #lib/theme.ts or a relative path), and reports the changes it does not make for you (svelte.config.js moved into sveltekit({ ... }), tsconfig.json extending $app/tsconfig). The installation guide and the agent skill gain a SvelteKit 3 section and import the theme in vite.config.ts with its .ts extension.

  • d9087e8: Select: an option whose value is "" (e.g. "All projects") is now a real selection. The trigger shows that option's label instead of the placeholder and drops the placeholder styling, so filters no longer need a sentinel value. The placeholder shows only when value is undefined or matches no option. onchange("") was already delivered and is now covered by a test. With required, a selected "" option counts as a selection.

2.1.1 latest

npmGitHub release

Patch Changes

  • 01b5188: Steps: a leading <strong>/<b> step title is a block, so its line no longer mixes the bold face with the list item's regular-face strut. That mixed line rounded to a different height between page loads, shifting content below each step by 1px.

2.1.0 latest

npmGitHub release

Minor Changes

  • 62d18f8: Theme: port a palette by role and pick the page paper, without raw --ca-* overrides.

    • PresetDefinition.colors: { light?, dark? } takes role colors (brand, background, sunken, surface, overlay, text*, border*, success/warning/info/error). A per-scheme brand regenerates that scheme's brand ramp; background also drives <meta name="theme-color">. Scoped to the preset, so islands and preset switching keep working.
    • paper: "mesh" | "plain" | "grid" | "dots" | "lines" replaces the brand glow mesh with flat paper drawn in --ca-outline-subtle.
    • New sketchbook design style: journal on grid paper, serif display, square outlined cards with hard shadows.
    • Opt-in @codeatlasdev/ui/styles/marks.css: hand-drawn .ca-mark-underline and .ca-mark-note (arrow), painted with --ca-mark-color.
    • @codeatlasdev/ui/astro re-exports designStyle, DESIGN_STYLES and mergeTheme, so astro.config can use a recipe.

2.0.0 latest

npmGitHub release

Major Changes

  • dbb7193: bits-ui is gone. Every interactive component now runs on the library's own headless layer (WAI-ARIA Authoring Practices, platform first): native <dialog> for Dialog, AlertDialog, Sheet, Drawer, Lightbox, CommandPalette and DocsSearch; native inputs for Checkbox, Switch and RadioGroup; own floating, dismiss, focus, roving-focus, listbox and presence primitives for Popover, Tooltip, DropdownMenu, Select, Combobox, Tabs, ToggleGroup, SegmentedControl, Slider, PinInput, Accordion, Collapsible, ScrollArea, Pagination, Avatar and the date family. Public props, classes and data-state attributes are unchanged.

    • Smaller bundles: AppShellLayout 41.2 → 35.1 kB gzip, form controls −18.7 kB, date family −45.2 kB.
    • No more ssr.noExternal for bits-ui and no WKWebView dialog workaround needed.
    • Select / Combobox with required block native form submit while empty and focus the trigger.
    • Disabled Slider now renders dimmed (the selector never matched before).
    • Interaction patterns were designed after studying Bits UI (credited in NOTICE).

    Breaking (upgrade notes)

    Props, exported types, ca-* classes and data-state attributes did not change. What changed is the DOM around overlays and the dependency set:

    • Modal overlays (Dialog, AlertDialog, Sheet, Drawer, Lightbox, CommandPalette, DocsSearch) are a single native <dialog> moved to <body>; the scrim is its ::backdrop. CSS that targeted a separate overlay/scrim element or the old portal container no longer matches. Style dialog.ca-overlay::backdrop instead.
    • --bits-* CSS variables and data-bits-* attributes are gone (--ca-anchor-width, --ca-accordion-height, --ca-collapsible-height, data-ca-day replace the ones used internally).
    • bits-ui is no longer installed with the package. If your app imports from bits-ui directly, add it to your own dependencies.
    • Select no longer renders bits-ui's hidden <input>; required still blocks native form submit.

1.2.0 latest

npmGitHub release

Minor Changes

  • e5b0fe8: Design tokens v2: every visual dimension is a theme knob, so one component set can render any design language.

    • New defineTheme options: controlRadius, numeric or named density (compact/comfortable/airy), border, typography (scale, weights, heading/label tracking and case, line height, measure), elevation (soft/none/hard/glow), motion (scale, easing personality), iconStroke, noise. designTokens() exposes the mapping.
    • Scales are parametric: spacing = 4px × step × --ca-density (new half steps 0.5/1.5/2.5/3.5 and 20/24), radius × --ca-radius-scale, type × --ca-font-scale, durations × --ca-motion-scale (0 under reduced motion). [data-ca-tokens] re-derives them inside an island.
    • Fluid layout tokens: --ca-gutter, --ca-section-gap, --ca-content-max, fluid display sizes --ca-font-size-display-1..3. Role tokens: --ca-radius-control, --ca-control-height*, --ca-border-width*, --ca-border-color, --ca-font-weight-*, --ca-heading-*, --ca-label-*, --ca-line-height-*, --ca-measure, --ca-icon-stroke, --ca-noise-opacity, --ca-ease-emphasized.
    • Material tiers read --ca-mat-{tier}-{filter,fill,shadow} and --ca-mat-edge. New surface styles: outlined, neumorphic, clay, tonal (alongside glass, frosted, solid, flat).
    • Design style recipes: designStyle(id, overrides) and DESIGN_STYLES for Swiss, Neobrutalism, Neumorphism, Glassmorphism, Material 3, Fluent 2, Apple HIG, Bento, Claymorphism, Y2K, Editorial and Terminal.
    • Headings read the theme's display family, heading weight, tracking and case; Lucide icons read --ca-icon-stroke.
    • One breakpoint scale (600/900/1200/1536) everywhere, enforced by the token lint, which also rejects malformed svelte-ignore comments.
  • e5b0fe8: RadioGroup: variant="cards" (selectable cards with label and description, whole card is the hit area) and aria-label. Option ids are now unique per instance, so two groups with the same values on one page no longer share label targets. CodeTabs: a plain code tab renders inside the framed .ca-code block as a focusable scroll region.

  • e5b0fe8: Remove the legacy theme editor generator: editorTokensToCSS, editorPreviewCSS, DEFAULT_CONFIG and the ThemeConfig type are gone, with no aliases.

    Replacement: defineTheme() + themeCSS() (or designStyle(id, overrides) for a recipe) cover every dimension the old hue/chroma/radius/spacing/shadow config exposed, and [data-ca-tokens] + [data-style] + [data-theme-scope] scope a theme to a single element. The theme editor at /theme/ is rebuilt on them: it starts from a design style recipe, edits any ThemeDefinition key and prints the exact defineTheme({ ... }) call to paste.

    Migrate: ThemeConfig.hue/chroma/lightL → presets: [{ id, color }]; radiusBase → radius; spacingBase/paddings → density; fontSans/fontMono → fonts; shadowIntensity → elevation; surfaceStyle → defaultStyle.

  • e5b0fe8: Viewport hardening:

    • --ca-vh-min (small viewport, svh with vh fallback) for full-screen sections, so a hero never hides its call to action behind the browser UI.
    • Focus is never obscured by sticky chrome: scroll-padding-block on :root accounts for the header, safe areas and --ca-sticky-top / --ca-sticky-bottom.
    • Prose heading anchors are always visible on touch screens (no hover-only affordance).
    • BottomNav labels use an 11px floor.
    • SimpleTable responsive="cards" captions every cell with its column header automatically; data-label only overrides.
    • caThemeHandle warns in dev when the viewport meta blocks zoom (user-scalable=no, maximum-scale=1).

1.1.0 latest

npmGitHub release

Minor Changes

  • f7d9a25: Accessibility foundations.

    • New SkipLink component (ca-skip-link). AppShellLayout, DocsShell and AuthLayout render it as the first tab stop and own the #main target. DocsShell keeps its ca-docs-skip class and docs.skipToContent label, but its <main> id is now main (was ca-docs-content): update any CSS or links that targeted #ca-docs-content. AppShellLayout gets mainId (default "main"; pass null when your page renders its own <main id="main" tabindex="-1">) and skipLink (set false when the shell is embedded in a page that already has one).
    • New announce(message, politeness) backed by one shared live region. CopyButton now announces through it (its local ca-copy__status element is gone), and Button/IconButton announce "Finished loading" when loading ends. New RouteAnnouncer in @codeatlasdev/ui/kit announces SvelteKit navigations (document title, or the page <h1>).
    • forced-colors: active support: glass, blur and shadows are removed, surfaces get a CanvasText border and focus uses Highlight.
    • prefers-contrast: more tokens (text-secondary and text-hint at 7:1 or more, outlines at 3:1 or more) are emitted per preset by the theme generator, so built-in and brand presets from defineTheme both get them. Built-in theme CSS is regenerated.
    • Combobox gets an ariaLabel prop to name the control when there is no visible label.
  • fa63ddd: Developer and agent tooling.

    • ca-ui-mcp: a local MCP server (stdio) with list_components, get_component, search_tokens and get_guide, backed by manifests generated from the component source. Works offline.
    • components.json and tokens.json are generated on every build and exported as @codeatlasdev/ui/components.json and @codeatlasdev/ui/tokens.json (props, types, defaults, snippets, public ca-* classes, --ca-* tokens).
    • codeatlas-ui CLI. init wires the theme, hook or integration, global.css, colorScheme.init() and the mobile viewport into a SvelteKit or Astro app without overwriting existing files (it prints a diff; --write applies it). migrate is a codemod that renames BrandPreset to PresetDefinition.
    • Dev-only [ca-ui] warnings for IconButton without an accessible name and Dialog without a title.
  • 0128aee: Performance foundations.

    • New Image component: required width/height (no layout shift), lazy by default, priority for the LCP image (eager, fetchpriority="high", preload link), decorative for presentational images, dev warnings for missing dimensions and alt="" without decorative.
    • defineTheme({ fonts }) accepts font descriptors (family, src, weight, display, preload, fallback, fallbackMetrics): the theme CSS emits @font-face plus a size-adjust fallback face, and themeHead/caThemeHandle emit <link rel="preload" as="font" crossorigin>. Plain stack strings keep working. New exported types FontDescriptor, FontFallbackMetrics, FontSpec.
    • New stylesheet entries styles/core.css (everything plus the default amber preset) and styles/presets/<id>.css (one built-in preset each). styles/global.css still imports everything.
    • Internal: bundle-size budget (bun run size), token lint (lint:tokens), both part of validate. Built-in preset sources moved from styles/themes/ to styles/presets/ (not a published path).
  • 7abc0f9: Add the @codeatlasdev/ui/seo entry point: a Seo head component (title template, canonical, robots, hreflang, Open Graph, Twitter, JSON-LD with < escaped), typed JSON-LD builders (organization, website with SearchAction, softwareApplication, product, faqPage, breadcrumbList, article, graph), sitemapXml and robotsTxt, llmsTxt and a markdownNegotiation SvelteKit handle for AI crawlers, ogImageRoute for 1200x630 social images through an injected headless browser, and PWA helpers manifest() and appleMeta(). caThemeHandle now warns in development when the final HTML has no viewport-fit=cover or no lang on <html>. esm-env is now a direct dependency (dev-only warnings).

Patch Changes

  • 7bcfd57: Browser chrome color sync no longer gets stuck when the page starts in the background (or WebKit pauses its frames): a timeout backs up requestAnimationFrame, and the color re-syncs when the tab becomes visible.
  • cd94a19: Browser chrome color sync re-samples after CSS transitions end, so the status bar no longer keeps an in-between color when a theme switch animates the header background.
  • c9099ba: CommandPalette: pressing Enter on a result closes the palette again with bits-ui 2.19. Closing returns focus to the element that opened it while the Enter key is still down, and the browser then activated that element and reopened the palette; the palette now closes on keyup. Also builds with @sveltejs/package 3 and TypeScript 6.

1.0.0 latest

npmGitHub release

Major Changes

  • ef015e6: 1.0.0: stable API. From here on, breaking changes to props, exports, --ca-* tokens or the public ca-* classes (see docs/css-api.md) only land in a major.

    Breaking

    • Default UI strings are English and come from a typed catalog (en, pt-BR). Portuguese apps call setLocale("pt-BR") once in the root layout; the wording is the same as before. Text props (label, placeholder, confirmLabel, emptyText, …) no longer have hardcoded defaults: omit them to use the catalog, pass them to override.
    • ThemeOptions is now Partial<ThemeDefinition> (defineTheme). BrandPreset is removed: use PresetDefinition ({ id, color, label? }). brandPresetCSS output changed (theme islands) and the head style id is ca-theme (was ca-brand-presets).
    • CSS is layered: global.css declares @layer ca.reset, ca.tokens, ca.themes, ca.base, ca.components, ca.materials, ca.utilities. App CSS stays unlayered and wins without !important; override a token by declaring --ca-* on :root.
    • Page background: html paints a solid --ca-surface-base and the decorative mesh moved to body::before (Safari 26 and the overscroll area sample the root color). body is transparent.
    • MobileHeader and BottomNav are opaque (--ca-chrome-surface) so the browser status bar and toolbar match them.
    • autofocus on touch devices only focuses when the keyboard was primed in the opening tap (primeKeyboard()); pass { touch: true } to force. This removes the iOS "focus without keyboard" jump.
    • responsive is undefined-width until init() runs (no hydration mismatch); derived values treat it as desktop.
    • The .ca-app-scoped 16px input rule and the .ca-input-ios-safe class are gone: every text field is covered globally.

    Theme engine

    • defineTheme() + themeCSS(): typed theme object compiled once to deterministic CSS: brand presets from one hex, fonts, radius multiplier, density, raw token overrides.
    • Built-in presets are generated from theme/builtins.ts (bun run --cwd packages/ui themes; validate fails if styles/themes/*.css is stale).
    • The init script validates stored preset/scheme/style against what the app knows (an old ca-preset no longer repaints the app in amber), applies data-style before first paint, sets <meta name="theme-color">, follows the OS under system before hydration and replays runtime tokens.
    • Theme islands: data-theme-scope="light" | "dark" and data-preset on any element, nested in any order.
    • colorScheme.setTokens(vars) writes one <style id="ca-runtime-tokens">, persisted and replayed before paint. colorScheme.init() is idempotent and returns a cleanup; storage errors (Safari private mode) no longer throw. colorScheme.presets lists valid ids.

    Mobile and WebKit

    • Tokens --ca-safe-top|right|bottom|left, --ca-vh (dvh with vh fallback), --ca-kb-inset, --ca-input-font-size(-sm), --ca-chrome-surface, --ca-bottom-nav-h.
    • viewport store (on-screen keyboard inset from visualViewport, ignores URL-bar and pinch-zoom changes) and the keyboardAware action.
    • primeKeyboard() / focusTextField() (from @codeatlasdev/ui/actions): open the iOS keyboard from the tap that opens a dialog, sheet or search. Used by DocsSearch, CommandPalette and Combobox.
    • Browser chrome color sync: theme-color follows the opaque color under the top edge (header, theme island or page) on Chrome/Samsung Android, Safari ≤ 18 and PWAs.
    • New components: StickyActions, MobilePane. Sheet swipe-to-dismiss (swipeToDismiss), AuthLayout mobileBrand.
    • 44px touch targets for every Button size, press feedback on touch, no tap flash, 16px minimum on every text field (no iOS zoom), -webkit-text-size-adjust.
    • Overlays contain overscroll; backdrop-filter falls back to opaque surfaces where unsupported.
    • nativeFeel() (kit): keyboard-only focus rings, no browser context menu or selection on chrome, .ca-selectable opt-in, optional zoom lock for desktop shells; html[data-ca-surface="transparent"] for vibrancy windows.

    Integrations

    • Astro: caUI(theme) from @codeatlasdev/ui/astro injects the theme before first paint and configures SSR.
    • CodeTabs accepts tabs as data (for MDX/Astro where Svelte context cannot reach children); CodeSnippet and code tabs follow the scheme of their context through dual shiki themes.

    Fixes

    • Glass lost its blur in production builds: Lightning CSS (Vite 8) drops backdrop-filter declared before -webkit-backdrop-filter. Order fixed everywhere and guarded by a test.
    • Dialog body no longer collapses to zero height in WKWebView (Tauri, Safari).
    • Select no longer puts aria-required on the trigger button; required state goes to the form control and the label.
    • isItemActive handles trailing slashes (trailingSlash: "always" sites).
    • Tooltip meets AA contrast in both schemes (--ca-tooltip-bg).
    • New tokens --ca-focus-ring, --ca-focus-ring-offset, --ca-selection-bg, --ca-scrollbar-thumb(-hover); reduced motion zeroes --ca-duration-*.

0.7.1 latest

npmGitHub release

Patch Changes

  • Charts now re-theme on a color scheme, preset or surface style change. Their options read design tokens from CSS custom properties, which aren't reactive, so a theme switch re-applied the previous theme's axis, text and palette colors. Token reads now track the theme store, so every chart option rebuilds with the new values.

0.7.0 latest

npmGitHub release

Minor Changes

  • Accessibility and behavior fixes found by the new test suite.

    • New tokens --ca-text-hint, --ca-brand-text and --ca-semantic-error-fill (WCAG AA contrast). Hints, placeholders and brand-colored text now use them.
    • DropdownMenu, Popover and Tooltip accept a triggerChild snippet that receives the trigger props, so a Button can be the trigger without nesting interactive elements. Button and IconButton forward extra attributes.
    • Checkbox and Switch accept aria-label and aria-labelledby. Switch is now 44×24 px.
    • Fixed: Combobox popup position and keyboard focus; MoneyInput lost typed input; use:mask fired input twice per keystroke; DataTable aria-sort, page reset and column toggle; colorScheme.resolved did not follow OS changes in system mode; calendar navigation labels in pt-BR; DatePicker popover focus; Toaster, NotificationBell, Select, FileUpload, ScrollArea and SettingItem ARIA issues.
    • CodeSnippet uses the high-contrast GitHub shiki themes.
    • DocsSearch results are real links again (<a role="option" href>), so middle-click, open in new tab and copy link work. A plain click still goes through onselect.
    • CommandPalette gains minQueryLength (default 1). The query is now trimmed before matching, so " rel " finds "Relatórios".

0.6.0 latest

npmGitHub release

Minor Changes

  • Docs primitives for developer documentation sites, mobile-first.

    • DocsShell: page frame with sticky glass header (brand, section tabs, search, theme), sidebar + "Nesta página" rail on desktop, and ONE compact header on phones (safe-area insets, 44px targets) whose menu opens a bottom Sheet with a section switcher and the sidebar tree. The page index becomes an inline disclosure on small screens. ⌘K / Ctrl+K / / open search; skip link included. Props: sections, active, groups, pathname, headings, searchIndex, onnavigate, brand, actions, menuFooter.
    • DocsNav (sidebar tree with MethodBadge), TableOfContents (variant="rail" | "inline", scrollspy), DocsSearch (dialog on desktop, full-height sheet with a 16px input on phones; accepts entries or an async loader) and the searchDocs ranking helper.
    • CodeTabs / CodeTab: language tabs with keyboard support, 44px targets, scrollable strip, copy button, and preference sync across groups (sync key, persisted in localStorage; first paint always renders the first tab so SSR hydrates cleanly).
    • Callout (note, tip, caution, danger; built on Alert), Steps, LinkCard, CardGrid, MethodBadge.
    • .ca-prose typography in global.css: headings with balanced wrapping, links, inline code that wraps, tables in .ca-table-wrap (scrollable, focusable), shiki dual-theme code blocks in .ca-code, tight mobile spacing, reduced motion.
    • Showcase: Navigation / Docs.

0.5.2 latest

npmGitHub release

Patch Changes

  • AppShellLayout: skip the desktop panel for modules with a single destination, so the rail item, panel entry and page title no longer repeat the same label.

0.5.1 latest

npmGitHub release

Patch Changes

  • Mobile polish.

    • Inputs no longer trigger Safari's focus zoom: TextField, TextArea, Select, NumberField, Combobox, CommandPalette and the DataTable search/page-size fields use max(16px, var(--ca-font-size-body2)) under (pointer: coarse), (max-width: 767px). base.css adds the same rule for raw input, textarea, select inside .ca-app and a .ca-input-ios-safe helper class. Zoom itself is never disabled.
    • New Sheet component: bottom sheet (bind:open, title, description, onclose, children, footer) with drag handle, safe-area padding, focus trap, Escape/backdrop close and reduced-motion support; centered dialog from 768px.
    • AppShellLayout gains mobileTitle snippet that replaces the plain page-title text in the mobile header (tappable session title, etc.).
    • AppList, AppSettings, AppForm, PageShell: inside AppShellLayout the h1 is visually hidden below 900px (kept for a11y) because the mobile header already shows the title; below 768px the actions wrap under the description, full-width.
    • EmptyState gains variant="hero": larger brand-tinted icon tile with glow, bigger title, actions stacked full-width on mobile.
    • Button: disabled filled keeps the brand hue (AA fill + contrast text at 0.45 opacity) instead of looking muddy.
    • AppSettings (mobile): tabs in the horizontal strip were inheriting width: 100% and each filled the row; they now size to content, with a 36px touch target, scroll-snap and a trailing edge fade.
    • AppList / useListState: new ofLabel option (default "of") for the word in the auto count and page info; pass "de" for pt-BR ("5 de 5"). On mobile the auto search field now fills the toolbar instead of keeping its 180px minimum.

0.5.0 latest

npmGitHub release

Minor Changes

  • AppShell (mobile): one compact sticky header. New mobileHeaderActions snippet on AppShellLayout renders at the header's trailing edge (switchers, account menu), so apps no longer need a second bar under the header. The header pads for env(safe-area-inset-top); BottomNav extends its background through env(safe-area-inset-bottom) over an opaque base (no dark band above Safari's toolbar), uses equal shrinkable columns with ellipsised 10px labels so five items fit at 320px, and has 44px touch targets. Apps must set viewport-fit=cover in their viewport meta for the insets to apply.

    Theme: new --ca-brand-fill / --ca-brand-fill-hover tokens in every static theme and in generated brand presets. --ca-brand-contrast text on --ca-brand-fill is at least WCAG AA (4.5:1): white is preferred, the fill is darkened (hue and chroma kept) when needed, and ink text is used only for very light brands. generatePreset now picks the text colour by real contrast instead of an L threshold; Button/IconButton filled, Badge brand, active Tabs, Pagination, Checkbox, Stepper and the nav badge now use the fill tokens. Disabled filled Button keeps the AA pair at 0.6 opacity.

    New Lightbox component: full-screen image viewer (bind:open, src, alt, onclose) with focus trap/restore, Escape, backdrop and close-button dismissal, safe-area insets and reduced-motion support.

0.4.4 latest

npmGitHub release

Patch Changes

  • Dark themes: --ca-brand-contrast is now white. Text and icons on brand fills (primary Button, IconButton, Badge, active Tabs, Checkbox, Pagination, Stepper, PanelNavItem) were near-black on the dark presets. APCA contrast on the dark brand fills: white Lc 70–84 vs near-black Lc 31–45 on every preset (WCAG 2 ratios favor black on crimson/cyan/emerald/ocean/slate, a known WCAG 2 error for mid-tone fills in dark mode).

    Dialog: new size="xl" (820px) for two-pane layouts such as settings.

    Kbd: flat keycap (tinted fill, heavier bottom edge) instead of a glass blur, which rendered as a near-black block on dark surfaces.

0.4.3 latest

npmGitHub release

Patch Changes

  • SvelteKit 3 support: useBreadcrumbs and usePageTitle read $app/state (the removed $app/stores broke Kit 3 apps at import), and caThemeHandle derives its Handle type from @sveltejs/kit/hooks so it type-checks on Kit 2 and 3. The @sveltejs/kit peer range is now ^2.12.0 || ^3.0.0. Hook return types are unchanged (Readable).

0.4.2 latest

npmGitHub release

Patch Changes

  • MobileHeader fills the width of the mobile shell bar instead of shrinking to its title (the bar background covered only part of the screen).
  • Tints no longer drift toward purple/pink. Every color-mix of a token with a neutral (gray tokens such as --ca-glass-tint, black, white) now mixes in OKLab: in OKLCH an achromatic color has hue 0°, so the hue was interpolated between the brand hue and 0°. Selected Chip, destructive Button/DropdownMenuItem hovers and other tinted surfaces now keep the brand or semantic hue.

0.4.1 latest

npmGitHub release

Patch Changes

  • Drawer: overlay no longer renders above the panel. Depending on CSS load order the base .ca-overlay z-index (modal) won over the drawer one and blurred the open drawer.

0.4.0 latest

npmGitHub release

Minor Changes

  • Brand themes and brand slot.

    • caThemeHandle(options) and caThemePlugin(options) accept ThemeOptions: defaultPreset, defaultScheme and presets, brand presets generated from one hex color and inlined in <head>. Before this, the init script always fell back to amber/dark and overrode data-preset set in app.html.
    • colorScheme starts from the configured defaults instead of hardcoded amber/dark.
    • PresetId accepts custom ids; BuiltinPresetId lists the eight shipped presets.
    • Charts use the brand hex as the first series color for brand presets (they fell back to amber).
    • hexToOklch and brandPresetCSS/themeHead/themeScript exported for custom setups.
    • tokensToCSS dark block now includes the neutral outline/glass/scrim tokens, so generated presets no longer inherit light-mode outlines in dark mode.
    • AuthLayout panel mixes brand with neutrals in OKLab: in OKLCH the hue swung toward 0° (blue brands rendered purple).
    • AuthLayout brand panel now hides below 768px (the override lost to the scoped rule); AuthCard footer links use the brand color.
    • ThemeToggle: light/dark button for the AppShell rail, SSR-safe icon.
    • brand snippet for the logo in AppShellLayout (top of the Rail and mobile header; MobileHeader gains leading) and in AuthLayout. Both default to the previous "CA" text mark.

0.3.0 latest

npmGitHub release

Minor Changes

  • Breaking: charts, DataTable and CodeSnippet moved out of the root import into subpaths with optional peers, so the core install no longer pulls ECharts, TanStack Table or Shiki.

    import from install
    @codeatlasdev/ui/charts echarts
    @codeatlasdev/ui/table @tanstack/svelte-table
    @codeatlasdev/ui/code shiki

    Migrate: change import { LineChart } from '@codeatlasdev/ui' to '@codeatlasdev/ui/charts' (same for DataTable*, Table, createCaTable, createCaColumnHelper, caTableFeatures, PaginationState, SortingState, Updater → /table, and CodeSnippet → /code), then install the matching peer. SimpleTable and UserCell stay in the root.

0.2.2 latest

npmGitHub release

Patch Changes

  • New README: AI-first quick start with npx skills add codeatlasdev/ui.

0.2.1 latest

npmGitHub release

Patch Changes

  • Charts load a modular ECharts build with only the series and components they use. The lazy chart chunk drops from 1,124 kB to about 650 kB (374 to about 218 kB gzip).

0.2.0 latest

npmGitHub release

Minor Changes

  • Package exports now point to dist (fixes @codeatlasdev/ui/vite failing to load from node_modules). Adapted to the bits-ui 2 API (Accordion drops collapsible), swapped deprecated lucide-svelte for @lucide/svelte, and exported the shared IconComponent/IconProps types plus TanStack PaginationState, SortingState and Updater. The theme plugin no longer warns about FOUC when caThemeHandle is set in hooks.server.

All notable changes to @codeatlasdev/ui are documented here.

Format based on Keep a Changelog. Versioning follows Semantic Versioning.


0.1.0 2026-09-26

Added

  • 60+ components across page templates, navigation, data, forms, charts, overlays, and feedback
  • Page templates: AppList, AppForm, AppSettings, PageShell: complete screen patterns in one component
  • Navigation shell: AppShellLayout with Rail, Panel, BottomNav, MobileHeader, SiteHeader
  • Layout primitives: Grid, Row, Stack: replace manual flex/grid entirely
  • Charts (8): LineChart, BarChart, PieChart, ScatterChart, HeatmapChart, GaugeChart, RadarChart, Sparkline
  • Data: DataTable (TanStack Table v9), SimpleTable, Pagination, KpiCard, EmptyState, FilterGroup
  • Forms (11): Button, TextField, TextArea, NumberField, Select, Combobox, Checkbox, RadioGroup, Switch, Slider, FileUpload
  • Auth: AuthLayout, AuthCard
  • Overlays: Dialog, Drawer, Popover, Tooltip, DropdownMenu, CommandPalette
  • Feedback: Toast, Spinner, Skeleton, Progress, ErrorPage
  • 8 built-in OKLCH themes: amber, ocean, crimson, cyan, violet, emerald, rose, slate: set via data-preset attribute
  • Liquid Glass material system: GlassCard, GlassSurface, GlassAppBar, LiquidGlass
  • CSS token system: --ca-* custom properties for color, spacing, radius, typography, z-index
  • SvelteKit helpers (@codeatlasdev/ui/kit): theme handle, breadcrumbs, page title, error templates
  • Vite plugin (@codeatlasdev/ui/vite): fixes SSR externalization for @tanstack/svelte-table
  • Dark mode: independent from themes, compose freely (data-preset="ocean" class="dark")
  • Svelte 5 runes throughout: $state, $derived, $props, $effect, no legacy APIs
  • .agents/skills/: AI agent skills for codeatlas-ui, DX, and writing

Special thanks

To our main supporters and special contributors, Fábio Henrique Araújo (@FabioHAraujo) and Richardson Douglas (@Richards0nd). 💛