Migration guide
Each section names what changed, why it breaks, and what to write instead. LLui is pre-1.0, so a minor release may break; the changelog records which release shipped each change.
Component product contract and styling paths
This section covers the component initiative that made @llui/components and the registry
two explicit, documented surfaces
(#261–#267,
#270,
#273). Items marked 0.20.0 shipped in
@llui/components@0.20.0; the rest arrive with the next release.
Checklist
- Replace a
theme-dark.cssorlayout.cssimport (stylesheet entry points). - Rename any baseline scale token you override (tokens).
- Stop assuming an
llui addname is an import path (discovery). - Pass an
idand placedirectionSync/exitCompletionwhere you use those machines (defaults). - Re-pull copied registry files you have not edited, and diff the ones you have (registry source).
- Update scripts or links that named the old demos (demos and scripts).
Stylesheet entry points
theme-dark.css is removed (0.20.0). theme.css now includes the dark tokens, so the
second import fails to resolve.
/* before */
@import '@llui/components/styles/theme.css';
@import '@llui/components/styles/theme-dark.css';
/* after */
@import '@llui/components/styles/theme.css';
If you imported only the dark half, import @llui/components/styles/semantic-tokens-dark.css
instead.
layout.css is removed; specialized-tools.css replaces it. The splitter moved out of
layout.css, and the pickers, editors, upload and canvas tools moved out of
form-controls.css, into the new specialized-tools.css. Only a modular bundle is
affected — theme.css already imports the new module.
/* before — a modular bundle */
@import '@llui/components/styles/semantic-tokens.css';
@import '@llui/components/styles/semantic-tokens-dark.css';
@import '@llui/components/styles/foundation.css';
@import '@llui/components/styles/form-controls.css'; /* also styled date-picker, time-picker… */
@import '@llui/components/styles/layout.css';
@import '@llui/components/styles/motion.css';
/* after */
@import '@llui/components/styles/semantic-tokens.css';
@import '@llui/components/styles/semantic-tokens-dark.css';
@import '@llui/components/styles/foundation.css';
@import '@llui/components/styles/form-controls.css';
@import '@llui/components/styles/specialized-tools.css'; /* pickers, splitter, upload, … */
@import '@llui/components/styles/motion.css';
Styling → entry points lists every module and the component scopes each one styles, generated from the stylesheets themselves.
The optional tailwindcss peer dependency is dropped (0.20.0). The Baseline theme never
needed Tailwind. Keep tailwindcss only if you use Registry skins, which need it for their
own build.
The two styling paths stay separate. Nothing changed about the rule: import theme.css
for the Baseline theme, or tokens.css + tokens-dark.css in your Tailwind v4 stylesheet for
Registry skins — never both.
Tokens
Baseline scales moved out of Tailwind's namespaces (0.20.0). The shadcn/ui base names
(--primary, --background, --radius and their pairs) are unchanged. The scales the
baseline reads were renamed, because Tailwind's namespaces mean something only inside a
Tailwind build:
| Before | After |
|---|---|
--radius-sm, --radius-md, --radius-lg, --radius-xl |
--llui-radius-sm … --llui-radius-xl |
--shadow-2xs, --shadow-xs, --shadow-sm, --shadow-md, --shadow-lg |
--llui-shadow-2xs … --llui-shadow-lg |
--transition-duration-fast, --transition-duration-normal |
--llui-duration-fast, --llui-duration-normal |
--z-index-popover, --z-index-dialog, --z-index-tooltip |
--llui-z-popover, --llui-z-dialog, --llui-z-tooltip |
--spacing-1 … --spacing-8 |
--llui-space-1 … --llui-space-8 |
--animate-accordion-down, --animate-accordion-up, --animate-caret-blink |
--llui-animation-accordion-down, --llui-animation-accordion-up, --llui-animation-caret-blink |
/* before */
:root {
--radius-lg: 1rem;
--z-index-dialog: 60;
}
/* after — reaches both paths: tailwind.css maps the Tailwind names onto these */
:root {
--llui-radius-lg: 1rem;
--llui-z-dialog: 60;
}
On the Registry skins path the Tailwind utility names (rounded-lg, z-dialog, …) still
work; tailwind.css maps them onto the --llui-* values with @theme inline, so override the
--llui-* token rather than the Tailwind one.
Disabled opacity is one token. Every disabled part in the Baseline theme now reads
--llui-disabled-opacity (default 0.5) instead of a per-family literal. Override it once.
(Registry skins keep shadcn's disabled:opacity-50 in each recipe.)
Status tokens. --info and --success join --warning and --destructive, and
--warning's light value darkened so a status glyph clears 3:1. Both toast skins take their
status colours from these tokens, so a theme that restyles status colours now restyles toasts.
Discovery: add names are not import paths
llui list used to print one name and a description per registry item, which read as if every
name were also a package subpath. It now shows the two surfaces side by side:
ADD NAME PRODUCT DISPLAY NAME CATEGORY ARTIFACT MACHINE IMPORT STYLING MODES
— accordion Accordion controls machine @llui/components/accordion baseline, styleless
accordion accordion Accordion controls skin @llui/components/accordion registry/Tailwind
calendar date-picker Calendar / Date Picker forms skin @llui/components/date-picker registry/Tailwind
dropdown-menu menu Dropdown Menu navigation alias skin → menu @llui/components/menu registry/Tailwind
form form-field Form patterns pattern adapter @llui/components/patterns/form-field registry/Tailwind
No llui add name was renamed or removed, so existing commands keep working. What changed is
that the relationship is now stated, and a few names you might have guessed never existed as
imports:
// before — guessed from the add name; neither module exists
import { dropdownMenu } from '@llui/components/dropdown-menu'
import { formField } from '@llui/components/form' // a different machine
// after — the machine each copied skin spreads
import { menu } from '@llui/components/menu'
import { formField } from '@llui/components/patterns/form-field'
These add names are aliases of a canonical component (generated from the product contract):
llui add alias |
Installs | Canonical component | Headless import | Gallery |
|---|---|---|---|---|
breadcrumb |
skin | Breadcrumbs (breadcrumbs) |
@llui/components/breadcrumbs |
open |
command |
pattern adapter | Command Menu (command-menu) |
@llui/components/patterns/command-menu |
open |
dropdown-menu |
skin | Menu (menu) |
@llui/components/menu |
open |
input-otp |
skin | Pin Input (pin-input) |
@llui/components/pin-input |
open |
resizable |
skin | Splitter (splitter) |
@llui/components/splitter |
open |
sonner |
skin | Toast (toast) |
@llui/components/toast |
open |
llui add breadcrumb # copies the Breadcrumbs skin; its machine is @llui/components/breadcrumbs
llui add command # copies the Command Menu pattern adapter; its machine is @llui/components/patterns/command-menu
llui add dropdown-menu # copies the Menu skin; its machine is @llui/components/menu
llui add input-otp # copies the Pin Input skin; its machine is @llui/components/pin-input
llui add resizable # copies the Splitter skin; its machine is @llui/components/splitter
llui add sonner # copies the Toast skin; its machine is @llui/components/toast
The component catalog lists every other difference — extra copied variants, pattern adapters, and machines with nothing to copy.
Defaults and component APIs
Automatic direction needs an id and a placed directionSync. tabs, carousel,
pagination, navigation-menu, menu, context-menu and menubar follow the page's dir
by observing their own root after mount. That observation is a Mountable; until you place it,
the component stays 'ltr' whatever the page says, with no warning.
// before
const t = tabs.connect(state.at('tabs'), send)
div({ ...t.root }, [div({ ...t.list }, triggers)])
// after
const t = tabs.connect(state.at('tabs'), send, { id: 'settings-tabs' })
div({ ...t.root }, [t.directionSync, div({ ...t.list }, triggers)])
menu.floatingDir is removed (placement reads state.dir), a menu's dir is never null,
and ContextMenuParts.trigger and MenubarParts.root gained a required id.
Submenus get real positioning. A hand-rolled submenu block still works, but renders pinned
to its container's top-left corner. Render each submenu with subOverlay instead, which
floats it beside its trigger and flips at the viewport edge.
// before
show(
openPath.map((p) => p.includes(item.value)),
() => [
div({ ...parts.subPositioner(item.value) }, [
div({ ...parts.subContent(item.value) }, children),
]),
],
)
// after
menu.subOverlay({
value: item.value,
state,
parts,
content: () => [div({ ...parts.subContent(item.value) }, children)],
})
Animated accordion and collapsible exits are opt-in, and the registry root takes the
watcher. init({ animated: true }) keeps closing content mounted until its animation ends.
The copied Accordion / Collapsible roots now take a required third argument:
// before
Accordion({ ...parts.root }, items)
// after
Accordion({ ...parts.root }, items, { exitCompletion: parts.exitCompletion })
Toasts.
// before
if (toast.paused) showPausedBadge()
Sonner({ ...t.root, variant: 'success' }, children)
// after
if (isPaused(toast)) showPausedBadge() // `pausedBy` holds the reasons
Sonner({ ...t.root }, children) // the colour follows the machine's data-type
toast.update's patch is narrowed to presentation fields (type, title, description,
duration, dismissable, ariaLive); id, remainingMs, status and pausedBy are
reducer-owned. A toast part's role, aria-live and data-type are signals now, so patching
type on a mounted toast also updates what it announces.
A two-phase overlay must keep its floating attachment. createOverlay throws a
LluiFrameworkError when floating and visibleWhen are combined without
floating.persistent: true:
// before — silently re-attached mid exit animation
createOverlay({ floating: { placement: 'bottom-start' }, visibleWhen, ...rest })
// after
createOverlay({ floating: { placement: 'bottom-start', persistent: true }, visibleWhen, ...rest })
Smaller changes.
rating-group'sclickItem/hoverItemrenamedisLeftHalftoisStartHalf(0.20.0), andslidermirrors underdir="rtl".table'srow(id, index),cell(rowIndex, colIndex)androwCheckbox(id, index)accept the row's index signal; the returnedaria-rowindex/data-row-indexare always signals. Pass the indexeachgives you instead of.peek()-ing it, or the row freezes at its first position after a reorder.combobox/searchableSelect:loadSuccessreplaces the whole list, and omittinggroupsresets them.- The baseline
foundation.cssno longer setsdirection: inheriton parts, so adiron a portaled positioner is respected.
The @llui/components README
carries the full notes for each machine.
Registry source you already copied
llui add never overwrites, so files you copied before this release keep their old recipes:
without the logical (RTL) utilities, the forced-colors variants, the 24px pointer targets on
small controls, and the recipe changes above. Re-pull a file you have not edited; for one you
have, compare first:
pnpm llui add switch --dry-run # shows what would be written
pnpm llui add switch --overwrite # replaces your copy
llui add checks @llui/* versions before writing (#273). Every registry item records
the minimum version of each @llui/* package it needs. An older installed version stops the
command with the upgrade to run; --force copies anyway and prints the mismatch as a warning.
These registry items need newer @llui packages than this project has:
@llui/components: installed 0.19.0, requires >= 0.20.1 (sonner)
Upgrade: pnpm add @llui/components@^0.20.1
Nothing was written. Pass --force to copy the files anyway.
Demos and scripts
The Component Gallery is the component inventory: it renders every component, with deterministic scenarios, on both paths, and the component catalog links each component to its gallery page. The two hand-written showcase apps are gone from that role:
| Before | Now |
|---|---|
"Components Demo" (examples/components-demo) |
Removed. The Gallery's Baseline theme document shows every component on that path. /examples/components-demo and its @llui/example-components-demo package no longer exist. |
"Registry Demo" (examples/registry-demo, /examples/registry-demo) |
A test fixture, not a showcase. It keeps the source llui add copied, so the repository can check that copy stays in sync with the registry. It is no longer published on the site. |
New root scripts in the LLui repository:
pnpm gallery # the gallery shell with both path documents
pnpm gallery:baseline # the Baseline theme document alone
pnpm gallery:registry # the Registry skins document alone
pnpm gallery:build # all three builds
pnpm check:registry now delegates to the registry package's own check, which also
type-checks registry/test/.
New, additive
@llui/cli/presentation-scenarios— the browser-safe scenario protocol the gallery and visual tests compile against the product contract.@llui/cli/gallery— the gallery's URL contract (galleryHref,parseGalleryQuery). Build gallery links with it rather than by hand.