Skip to content
LyEve UI v0.60.1

Getting started

Changelog

Every release of @lyeve-labs/ui-kit. Versions follow SemVer: while we're under 1.0 the minor carries new components and new API, and any breaking change too. The patch carries fixes. Nothing exported at 0.13.0 has been removed or renamed since.

0.60.1 Latest 2026-11-10

Changed

  • A focused field shows focus by its border alone, which turns the focus color at its own 1px. The 1px ring inside it made a 2px edge, heavier than the 1px edge of a highlighted option in a list. A field in error or holding a suggestion keeps its red or violet border, so it still adds a 1px ring in that color inside the border while it has focus. This holds for Input, Textarea, PasswordInput, SearchInput, NumberInput, Select, Autocomplete, DatePicker, ColorField, MultiSelect, CopyField and the search box at the top of a list.

0.60.0 2026-11-10

Changed

  • An empty checkbox, radio or toggle rests on line-strong at 60%, about 2:1 on every ground, and firms to full line-strong under the pointer. Full line-strong drew a gray frame round every box. This covers Checkbox, CheckboxGroup, RadioGroup, Toggle, the option boxes in MultiSelect and the boxes in PluginCart.
  • An option card in Checkbox, CheckboxGroup and RadioGroup rests on the soft line border and firms to line-strong under the pointer. A selected card keeps its brand tint and no longer draws a brand border, so the filled box or dot inside it marks the choice.
  • A focused field draws one 2px edge in one color: the border turns the focus color and a 1px ring just inside repeats it. The 2px half-strength ring inside the border read as a second line. A field in error keeps one red edge while it has focus, where it drew a blue ring inside the red border, and a field holding a suggestion stays violet. This covers every text field, select and autocomplete, NumberInput, MultiSelect, CopyField and the search box inside a list panel.
  • The FileInput drop zone shows focus by closing its dashes into a solid line, in the focus color outside an error, with no ring beside it.

0.59.0 2026-11-10

Changed

  • Every field rests on the soft line border a Card and a Button draw and firms to line-strong under the pointer: Input, Textarea, SearchInput, PasswordInput, NumberInput with its steppers, Select, Autocomplete, MultiSelect, DatePicker, TimePicker, ColorField, CopyField, the FileInput drop zone and the search box inside a list panel. Focus still moves the border to the focus color. A frame focused through a part inside it keeps its focus border under the pointer. Checkboxes, radios and toggles keep line-strong, because an empty one has nothing else to mark it.
  • The chosen SegmentedControl segment is a brand-tinted fill with brand text, drawn the way the sidebar draws the current page, instead of a raised chip with a thin brand ring. The track rests on line.

Fixed

  • A Tooltip stays inside the browser window. A hint on a trigger near an edge moves along that edge to stay 8px inside it, and a hint with no room on its own side opens on the opposite side when that side has room.

0.58.0 2026-11-10

Changed

  • The secondary and outline Button variants and the Select trigger draw the soft line border a Card draws instead of line-strong. A button's label and a select's value and chevron identify the control, so the border does not have to. The Select trigger firms to line-strong under the pointer. Text fields, checkboxes, radios, toggles and SegmentedControl keep line-strong, because their border is what marks them.

0.57.0 2026-11-10

Added

  • A focus color, the one color for every focus ring, focus outline and focused field border, used as ring-focus, outline-focus and border-focus. It is brand-deep wherever the control sits, so a part of a page that sets its own brand draws its rings in that brand.

Changed

  • Focus rings, the focus outline and a focused field's border draw in brand-deep instead of brand. In the dark palette a ring reads 5.59:1 on the page instead of 10.43:1, so a ring round a menu row or a chosen segment no longer outshines everything else on the screen, and it still clears 3:1 on every surface. A field's inner ring is the same color at half strength. A theme that sets brand also sets brand-deep, or its rings keep the kit's color.
  • The row the keyboard is on in a Dropdown fills like a hovered row under its ring.
  • The panels of Dropdown, Select, Autocomplete, MultiSelect and DatePicker draw the soft line border that AccountMenu, Modal and Card draw. The shadow and the lit top edge set them apart from the page.
  • Scroll bars take the palette's colors.
  • checkPalette reports a focus ring problem under brand-deep, the color the ring is drawn in, where it reported brand.

0.56.0 2026-11-09

Added

  • Card takes an image, drawn edge to edge above the heading and clipped to the card's top corners. With its own width and height it fills the card's width at its own shape, and without them it is cropped to a ratio, 16:9 unless the image names another.
  • NavBar, Footer and Gallery for a page drawn for visitors, rendered on the server with no script and part of site.css. The navigation bar's links wrap onto more lines on a narrow page instead of hiding behind a menu, and it marks the current page. The footer names each social profile by its network as a plain link. The gallery crops its images to 4:3 in a grid, or in a row that scrolls sideways, stops on each image and takes keyboard focus.
  • Components for a page drawn for visitors, each rendered on the server with no script: Section, a band with an introduction, content and a footer, an optional background image and a light or dark overlay, Columns, which stack once a column would fall under the site's minimum width and take an optional cap of two, three or four, Prose for markup written elsewhere, Image, which reserves its box from its width and height or a ratio, and Quote. Each is part of site.css.
  • data-scheme="dark" or "light" on any element draws everything inside in that palette, whatever the page's theme. A Section with an overlay names the overlay's scheme, so its body text, muted text and links clear 4.5:1 over any image.
  • Site tokens the new components read: --spacing-site-xs to --spacing-site-xl, set from --spacing-site-md, --spacing-column-min, --spacing-gutter, and --color-brand-2, a second color that follows violet until a site sets its own.
  • FormAssist wraps a form's fields so an assistant on the page can read them and suggest a value for each. A suggested value goes into its control, drawn in violet with a Suggested tag and buttons to accept or reject it, and a bar above the fields accepts or rejects them all. While a suggestion waits, the form does not submit and the bar asks the person to review it first. A value the person changed is never overwritten, and a password, a file and any control given noAI are never reported or filled. A suggestion a field was holding when it leaves the page, behind a tab or a closed section, is rejected and the old value comes back. activeAssistForm() returns the form to work on, the one in an open dialog or drawer before the page's, through a controller that can describe, suggest, accept all, reject all and list what is pending. assistField() registers a control the kit does not ship.
  • Every field control takes noAI, which keeps its value from an assistant and marks it data-no-ai for a browser agent.
  • Toggle, MultiSelect, DatePicker and FilterChips take name and post their value with the form.
  • AppShell takes a panel on the right, opened with panelOpen and named by panelLabel. From the lg breakpoint it docks beside the main area, which narrows to make room, and its edge drags or steps with the arrow keys between 320 and 560 pixels, remembered under panelStorageKey. The cap shrinks so the main area keeps at least 480 pixels. Below that, the same panel opens as a modal drawer from the right, so its content stays mounted as the window crosses the breakpoint, and a panel that has never opened renders nothing.

Changed

  • Every kit radius rung, xs to 2xl and card, is a multiple of --radius-md, so a page that sets it once moves the whole scale. At its default of 8px every rung keeps its value.
  • SectionHeading at both levels and the title of a Card draw in --font-heading, a new token that is the body face unless a page sets its own.
  • Breaking: Accordion is built on details, so the browser opens and closes each item and an accordion works on a page that runs no script. Its items share a name unless multiple is set, which has the browser close the others when one opens. An item's body fades in at each opening and shuts in one frame, where it used to grow and shrink to its height. The header is the item's summary, which holds the title as plain text, and it is no longer an h3 around a button with aria-expanded and aria-controls. A heading inside a summary hides the item's state from VoiceOver on iOS, drops out of heading navigation in JAWS and TalkBack, and keeps the item shut when a reader presses Enter on it in browse mode with Orca in Firefox. The titles are no longer in the page's outline, so put a heading above the accordion to name the group. The body has no region role, and the accordion-header- and accordion-panel- ids are gone, because the browser reports an item's state itself. An item's id is now the id of its details element, unique on the page: find an item by it and read its open property. A page put together from several server renders gives each its own idPrefix, or two accordions on it share a name.

Fixed

  • The account menu's panel never played its entrance in Chromium, which hides a shut panel without dropping its style, so the animation ran once while the panel was hidden. It now plays at each opening. A closed menu inside an open part of the page no longer draws its button lit and its chevron turned.

0.55.0 2026-11-09

Added

  • SplitPane takes a header snippet in place of its label in the header row, for a pane whose own tabs already name what it holds. The label still names the pane to a screen reader, on the strip and on its controls. The row stays one control tall, and in a pane dragged narrower than its tabs they scroll in their own strip while the lock and the fold stay in place.
  • Tabs takes inHeader for a strip in such a header row: it draws no rule of its own and its tabs take the row's height.

Changed

  • A SegmentedControl keeps each label on one line. When its options do not fit beside each other, in a narrow panel, a flex row or a grid column, it stacks them, and it lies back down once there is room.
  • A Card clips its content to its corners only with pad="none", so a list that opens from a field in a padded card draws over the page instead of growing the card. The footer band rounds its own corners.
  • AccountMenu closes when a link in it, or a button that posts a form, is chosen, and gives focus back to its trigger. A click canceled on the row before it reaches the menu leaves it open.

Fixed

  • A PageShell with fill no longer scrolls the shell when a visually hidden label sits in a scrolling region inside it, and every scrolling region in the kit is positioned so such a label stays inside it.
  • A panel opened from a field (Select, DatePicker and the rest) stays inside the room the reader can see, inside any scrollbar of the region that clips it. When it runs past an edge it aligns to its field's other edge first, then slides back with an 8px gap. A panel that fits is left aligned with its field.
  • A NumberInput stepper at its bound fades only its glyph and keeps its frame, where it drew a dark block.
  • DatePicker gives focus back to its field when a pick or Escape closes the calendar with focus inside it, with a focus ring only after a keyboard pick.

0.54.0 2026-11-08

Added

  • @lyeve-labs/ui-kit/site.css is the stylesheet compiled for a page with no Tailwind build: the theme in all three schemes and every class of Accordion, AccordionItem, Badge, Button, Card, Checkbox, Field, FormMessage, Input, Label, SectionHeading and Textarea.

Changed

  • Every Button variant carries a 1px border, transparent where none shows, so every variant is one height at each size: 34px for sm, 38px for md and 46px for lg. A filled button drew 2px shorter than an outlined one, and md now matches the 38px height of Input. In forced colors a ghost button shows its edge.

Fixed

  • The close button of Drawer and Modal is a 24px target under a mouse, up from its 16px glyph, and the glyph stays where it was.
  • CopyField with wrap breaks a value after a hyphen, and inside a token only when the token is wider than the line, so a UUID no longer splits inside a group. The copied value is unchanged.
  • The built package refuses a tool's cache, so a node_modules or .vite folder left under the sources no longer reaches the published files.

0.53.0 2026-11-08

Added

  • StepIndicator takes label, the list's accessible name, which defaults to "Progress". Name each list when a page holds two.
  • data-theme="system" on <html> paints the light or dark palette the operating system asks for, with no script, and repaints when that setting changes. getTheme() reads it the same way. A light override for such a page goes inside @media (prefers-color-scheme: light), as the README shows.

Changed

  • The close button on Alert, Banner and each toast is named "Close" instead of "Dismiss".
  • Default labels name what a control acts on. With a label, the clear button on Autocomplete reads "Clear" and the label, the NumberInput steppers read "Decrease" and "Increase" and the label, and CopyField keeps an acronym such as "API key" in its button name rather than lowercasing it. Without one they keep the bare verb.
  • Default placeholders are plain words without an ellipsis: "Search" on Autocomplete and SearchInput, "None selected" on MultiSelect and "Choose a date" on DatePicker, whose dialog is also named "Choose a date".
  • FileInput reads "Choose a file or drop it here", or "Choose files or drop them here" when it takes several, in place of "Click to upload or drag & drop".
  • The AppShell rail toggle reads "Expand navigation" and "Collapse navigation" in place of "Show sidebar" and "Hide sidebar".
  • PluginCart reads "No capabilities chosen yet. With none, you stay on the Free plan." when nothing is chosen, names each remove button "Remove" and the capability, and shows "Waiting for a price" without a period.
  • A copy that fails says "Could not copy. Try again." on CopyButton, in place of "Copy failed". CopyField says "Could not copy. Select the value and copy it yourself.", or "Could not copy. Show the value, then select it and copy it yourself." while its value is masked.
  • DatePicker shows the chosen date as "Sep 3, 2024" in every browser, where it followed the browser's locale, and its month grid always holds six weeks, so the panel keeps its height from month to month.
  • Dropdown, AccountMenu and DatePicker close when a press lands outside them, on pointer down rather than on click, and listen for it only while open.
  • Label draws the same label as every field: sentence case at the body size, where it was small uppercase.
  • The Card footer takes the header's 16px vertical inset, where it had 12px.
  • Stat draws its trend arrow as an icon and says "Up", "Down" or "Unchanged" to a screen reader. Button's spinner is 14px or 16px, the icon sizes beside its text, in place of 15px or 18px. The cross on a removable Tag is 12px, where it was 10px.
  • The close and chevron marks across the kit are drawn from @lucide/svelte.
  • The count on a Collapsible is drawn as a Badge, so it gains a border and medium weight.
  • The Drawer close button sits 2px lower, level with the Modal one.
  • AppShell, GateNotice, SectionHeading, Table and Tabs no longer carry the data-testid values app-section, app-sidebar-toggle, gate-notice, section-actions, table-frame, table-scroll and tabs-scroll. Select those elements by role or name.

Fixed

  • A FileInput with no id gets one of its own, so its label names the input.
  • The Drawer close button draws the same rounded focus ring as the Modal one.
  • SortableList listens for pointer events on the window only while a row is pressed or dragged.

0.52.0 2026-11-05

Added

  • motion.swapPage(), for a router to call just before it replaces the page. Every motion preset plays when an ancestor removes its surface, and the page is one, so a page holding an Alert, a Banner or an open overlay stayed on screen until that exit ended, with the new page below it. After swapPage() every preset plays in no time until the next frame, so the new page takes the old one's place at once.
  • checkPalette, which runs the contrast rules the kit's own palettes pass over an app's own colors. It takes the tokens an app overrides for each theme, lays them over the kit's values and returns every pair below its WCAG 2.2 floor. It measures text on the grounds the kit paints, from the center of each canvas pool and the hero to the current, head and held rows and the danger panel. It measures badges, avatars and row chips, and a filled button's label at rest, hovered, held and under its lift. At 3:1 it measures the focus ring, a control's border and a hovered button's edge. Each result names the theme, where the pair is painted, the two tokens, the ratio and the floor, and an empty list means the palette passes. It reads #rgb and #rrggbb, and it throws on any other format, an unknown token and an unknown theme.

Fixed

  • SidebarNav opens a group the current page moved into, and shuts one it left, in one frame. Only a group the reader opens or shuts slides, so a page change no longer moves the rows under the pointer.
  • A page printed in the light theme kept an app's own colors whenever the app set its light palette in a rule after the stylesheet, as the kit's docs show. The print palette's values are now important, so such a page prints black on white.
  • The README's palette override set only the dark theme, so a light page kept the kit's colors. It now sets both, with colors that pass checkPalette.

0.51.2 2026-11-04

Changed

  • The dark palette's raised layers mix 2% of the dark blue (#003366) into the navy, in place of the 5% of 0.51.1, for a softer blue. surface is #0c1728, surface-2 is #121e30 and line is #192a3d. The line sits one step above the exact mix so it stays 1.23:1 against the surface. ink, the brand colors and the light palette are unchanged, and every text and control border pair still clears WCAG AA.

0.51.1 2026-11-04

Changed

  • The dark palette's raised layers are navy with 5% of a dark blue (#003366) mixed in, in place of the muted blue of 0.51.0, which leaned toward cyan. surface is #0b172a, surface-2 is #111f32 and line is #192a3f. They keep the lightness of the navy in 0.48.0 with the hue moved slightly away from violet. ink, the brand colors and the light palette are unchanged, and every text and control border pair still clears WCAG AA. 0.51.0 was not published to npm.

0.51.0 2026-11-04

Changed

  • The dark palette's raised layers are a muted blue instead of gray. They hold less color than the navy of 0.48.0 and lean further from violet. surface is #0b1723, surface-2 is #121f2b and line is #1c2938. Each keeps the lightness it had, and ink, the brand colors and the light palette are unchanged. Every text and control border pair still clears WCAG AA.

0.50.0 2026-11-04

Added

  • AccountMenuItem, one row of AccountMenu, drawn like a resting SidebarNav row: an optional 16px icon in its own column and a muted label that turns to full text color on a surface-2 fill under the pointer, with no border, shadow or fill at rest. With href it is a link and passes target and rel through. Without one it is a button, type="submit" for a sign-out form. loading disables the button, marks it busy and shows a spinner in place of the icon.
  • AccountMenu takes an optional footer snippet, drawn under a rule after the rows, for the form that ends the session. The rows and the footer are spaced like the sidebar's rows.

Changed

  • The AccountMenu trigger keeps its hover fill while its panel is open, so the panel reads as coming from it.

0.49.0 2026-11-04

Changed

  • The dark palette's raised layers are gray instead of navy, so a card no longer reads violet. surface is #11171d, surface-2 is #191e25 and line is #222931. Each keeps the lightness it had, and ink, the brand colors and the light palette are unchanged. A hovered table row still reads apart from the head row and from a resting row, and every text and control border pair still clears WCAG AA.

0.48.0 2026-11-04

Changed

  • The dark palette's raised layers are darker, so a card no longer reads gray against the page. surface is #0c1627, surface-2 is #121e2f and line is #1a293c. A hovered table row still reads apart from the head row and from a resting row, and every text and control border pair still clears WCAG AA.
  • The dark card no longer carries a white wash at its top. bg-card is none in the dark palette, so a card is its plain surface color. The light palette's card is unchanged.
  • The highlight along a raised surface's top edge is half as bright in the dark palette, white at 6% instead of 12%.
  • AuthShell sits on the same ink and bg-canvas light as the app shell's content, so a sign-in page matches the console and the sites instead of a gray-violet wash.
  • The hero wash under EmptyState is half as strong, 5% of violet into the ink instead of 10%.

0.47.0 2026-11-04

Fixed

  • SidebarNav and Breadcrumb refuse an item's href that a browser would run as script, the way the kit's other link components do. That covers a javascript:, data: or vbscript: URL, including one with leading whitespace or a tab or line break inside the scheme, where Breadcrumb used to catch only the first two written plainly. The item still renders, without a link. A relative or https href renders as given.
  • A hovered Table row takes surface-2, so it no longer reads as a second head row and is no longer outlined by a ring that crossed the frame's rounded corners. A row of header cells only takes no hover.

Changed

  • Focus no longer glows. A focused button, link, card, tab or menu row shows its 2px brand outline or ring and nothing around it.
  • A focused text field shows its brand border with a soft ring inside it, at 35% of the brand, and no glow outside it. A date or time segment and an icon button inside a field keep the full brand ring, because nothing else marks their focus. The file drop target now turns its border brand on focus, as the other fields do.
  • The primary button no longer sits in a pool of light or grows a glow on hover, press or focus. It keeps its fill, its hover and press colors and its top bevel, casts the same small shadow as the other filled buttons in every state, and the light over its fill is fainter. The glow and primary shadow tokens stay defined, and the two primary ones now hold the raised shadow.
  • A clickable card answers the pointer with its brand border alone, with no glow.
  • A nested sidebar row is drawn exactly like a top-level row at rest, under the pointer, on focus and as the current page. Its fill and its current bar now start at its own indent instead of in its section's column, and the current row no longer carries an edge a hovered row lacks. One row is marked at a time. While the section that holds the current page is open, the page's row carries the mark, and the section shows its label in full text color with no bar of its own. Once you close that section, the section carries the page's mark until you open it again.
  • A Table's head row takes the new band token, the brand at 8% in the surface, slightly deeper than before.
  • The Table frame drops the card's top wash, so its rows are one color at rest.

0.46.0 2026-11-03

Added

  • Depth tokens in both palettes, each with its Tailwind utility: the colors highlight, shade, glow, halo and tint, a shadow ramp from shadow-raised through shadow-card and shadow-popover to shadow-overlay, shadow-glow, shadow-mark, shadow-primary and shadow-primary-hover, the insets inset-shadow-highlight, inset-shadow-bevel and inset-shadow-well, rounded-card, leading-title, and the background images bg-canvas, bg-sidebar, bg-card, bg-hero, bg-lift, bg-accent, bg-accent-y, bg-rule-end and bg-rule-y.

Changed

  • Surfaces carry depth in the same palette. Card, Panel, Stat and Table sit on the card shadow with a lit top edge, the 20px card radius and a gentle gradient laid over their background color, so a background a page passes still shows. Menus, tooltips, toasts and a dragged SortableList row take the popover shadow, and Drawer and the dialogs the overlay shadow.
  • AppShell lights the top of its content column with a faint cyan and violet wash, gives the sidebar its own gradient and a right edge that fades at its ends, and draws the header over the wash with a rule that fades toward its end edge.
  • The primary Button has a vertical gradient, a bevel and a brand glow at rest that grows on hover. Every focus ring glows cyan, and fields and SegmentedControl sit in a shallow well.
  • The current SidebarNav row has the brand tint, the cyan to violet accent bar and a faint glow. The active tab in Tabs and the Progress fill carry the same accent, and every horizontal accent is mirrored in right-to-left layouts.
  • A Stat tile carries a 2px cyan to violet line along its top edge, and a Table header row and a hovered row take the brand tint. A Badge dot carries a soft halo of its own tone.
  • EmptyState and AuthShell sit on a hero wash of ink into a tenth of violet.
  • The PageHeader title is 32px on a 38px line, semibold with tighter tracking, and 24px below the sm breakpoint. Section eyebrows step up to the muted color and semibold.
  • Every addition is a shadow or a background image, so nothing changes size and nothing shifts. Forced colors and print drop the gradients, glows and shadows, and every text pair the change adds clears WCAG AA in both palettes.

Fixed

  • Progress stays visible in forced colors. The track draws a border and the fill paints in the system text color, where both used to vanish.
  • The primary, violet and danger buttons show their boundary in forced colors, with an outline that takes no space. They had none.

0.45.0 2026-11-01

Added

  • PluginCart takes charged, the price in cents the order will charge. A host that prices a discount code apart from the cart's quote passes it, and while the quote still describes the selection it stands in for the quote's total in the sticky bar and the default summary, and is the total the summary and actions snippets receive. Without it the cart reads the quote as before.

Fixed

  • Tabs is one stop in the tab order. The selected tab holds it, Left and Right move the focus along the strip and wrap at the ends, and Home and End go to the first and last tab. Moving does not choose a tab: Enter, Space or a click does.
  • Between the md and lg breakpoints the AppShell rail opens only once a pointer has rested on it for 100 ms, so a pointer crossing it toward a control beside it no longer opens the navigation column over that control. Focus still opens it at once.
  • The scroll edge fade on a table or tab strip takes the foreground token, so it lightens a dark card as it darkens a light one. In the dark theme it could not be seen.
  • Pagination built from links draws a step with nowhere to go as a disabled button, as the callback pager already did, instead of a link with no target that assistive technology could not name.
  • AccountMenu gives its trigger an accessible name, the person's name and the menu's. Below the sm breakpoint it shows only the avatar, which is hidden from assistive technology, so it had none.
  • Table fades a row's hover ring in with its tint, on the fast duration and the move curve. The ring appeared ahead of the tint.

0.44.0 2026-10-28

Added

  • Alert and Banner take announce, which reads a message out when it appears, and every change to it after, without clearing it. Pass it for a result that stays on screen, such as a confirmation that replaces a form or a status a poll updates.

Changed

  • No component carries role="status" any longer. Toaster, SortableList, CopyButton, CopyField, FormMessage on its success tone, and Alert and Banner that report an outcome are each a polite region with aria-live="polite" and no role. Chrome and Firefox expose a status role as a status bar, and the Orca screen reader speaks nothing inserted into one. A test that finds one of these regions by its role finds it by aria-live now.
  • Alert and Banner that neither clear themselves, take announce, nor report a failure or a warning state a standing condition and are no live region, so a page that shows one, on load or after a client-side navigation, reads nothing out. A failure and a warning keep role="alert".
  • The text of an outcome arrives two frames after its region mounts, in FormMessage (one frame before) and in an Alert or Banner that clears itself or takes announce, and the region is atomic, as the status role made it. A page that renders an outcome inside {#if form} with its text already in it is then still a change a screen reader reads out. A consumer test that reads that text right after render waits for it with findByText.

Fixed

  • Toaster, SortableList, CopyButton and CopyField are heard in the Orca screen reader in Chrome and Firefox. SortableList and CopyField announce through a div, which Orca reads even when other events from the page come just before it. A paragraph lost the reorder sentence in Firefox that way.
  • SortableList speaks a move made with the keyboard or the handle's menu two frames after the handle takes the focus back. Firefox reports that focus as new, and Orca dropped a sentence that arrived just before it.
  • FormMessage is heard in Firefox. Its region was filled on the first frame, and Firefox saw it arrive already full.
  • Tabs scrolls only its own strip to show the active tab. It used scrollIntoView, which also moved the page, so a strip placed low on a long page pulled the page down to it as soon as it mounted.
  • Logo draws the bottom row of the mark. Its two lowest tips sat one unit past the view box and were cut off.

0.43.0 2026-10-25

Added

  • SortableList puts rows in order by dragging a row's handle or by pressing Alt with the up or down arrow. A mouse lifts a row after a few pixels of travel. A finger lifts it after resting a quarter of a second on the handle, so a swipe that starts there still scrolls the page. A line marks where the row will land, the box that scrolls the list scrolls near its edge, and Escape or a second finger puts the row back. Every move, and every move that cannot happen, is announced in the row's own words: "Hero moved to position 2 of 5." canMove refuses a move by returning the sentence to say instead, and the line turns to the danger tone over a refused place. The handle also opens a menu with Move up and Move down, from a click, a tap, Enter or an arrow, so a row moves without a drag or a chord. Alt with an arrow inside a text field stays with the field, where Option and an arrow move the caret on macOS, and so does Alt with an arrow that a control in the row takes for itself.
  • ColorField takes a color typed as hex, such as #2F6FEB, 2f6feb or the short #26E, and stores six upper-case digits after a #. Text that reads as no color is refused when the reader leaves the field. With swatches it is a radio group whose value is the picked swatch's value, such as a theme token's name. A stored value that no swatch carries any longer is kept, named and sent with the form until the reader picks another. It never draws the browser's color input. normalizeHex is exported for a host that reads hex itself.
  • DiffView lists the changes between two versions, each labeled Added, Removed, Changed or Moved in words, and sets a changed value's old and new text one above the other. A value longer than 400 characters shows its first 400 and a Show all button, and the cut falls between characters a reader sees as whole. Each Show all names its value to a screen reader, and every value folds again when the caller passes other changes. An emptied value reads "Empty". Every label and value takes its direction from its own text. With no changes it says the versions are the same.

0.42.2 2026-10-23

Fixed

  • PluginCart with per="once" prices each plugin card as "$3.00 once". The cards read "$3.00/mo" and promised a renewal that a one-time payment never charges. Monthly and annual carts keep "/mo" on the cards.

0.42.1 2026-10-23

Fixed

  • Button with an href calls its onclick. The link dropped the handler, so a host could not handle the click in place, such as choosing an item without leaving the page.
  • PluginCart takes per, what the total is charged per: 'monthly', 'annual' or 'once'. It defaults to period. A one-time payment is quoted monthly and charged once, so its total read "$6.00/mo" and promised a renewal. With per="once" it reads "$6.00 once".
  • PluginCart's review bar on a narrow screen reads "2 picked · $6.00", spaced and in the body face, and its link reads "Review order".

Changed

  • PluginCart sets every price in the body face with tabular numerals instead of the monospace face, so the page keeps one typeface.

0.42.0 2026-10-22

Added

  • PluginCart takes a summary snippet that draws the host's own summary in place of everything inside its aside: the title line, error, the list, the ladder, the total, notice and actions. It suits a host that prices more than the selection, such as a plan change or a discount code. The snippet receives the selection, the server total and a remove function that takes one selected capability out and reports it through onchange, as unticking its card does, and leaves a capability that is not selected alone. The total is null while a price is in flight and before one is asked for, so the host tracks its own request. The aside keeps its id, its stickiness and the bar that jumps to it on a narrow screen.
  • PluginCart takes a heading, the panel's name: the accessible name of its aside, also printed atop the default summary. It stays "Your selection" when unset, and the default markup is unchanged.

0.41.1 2026-10-22

Changed

  • The package is developed, tested and built on Node 26.12.1 with pnpm 11.25.0 only. It still supports Node 24 or later and any package manager for the people who install it, and its built output is unchanged.

Security

  • The development tools moved past every advisory pnpm audit reported, vitest and Vite among them. None of them ships in the package.

0.41.0 2026-10-18

Added

  • SplitPane docks a column to the start or end of a page. Above its breakpoint the inner edge drags, and the handle is a focusable separator that reports its width and moves by 24px on an arrow key, 80px with Shift, and to the smallest and largest width on Home and End. In a right-to-left page the edge follows the arrow the way it looks. A lock fixes the width, a collapsible pane folds to a strip carrying its name, and below the breakpoint it stacks in the page, hides, or floats over the page as an overlay. Collapsing or expanding the pane from its own button moves focus to the button that undoes it. The width and the lock are kept in local storage under the key the page names, as {"width": <px>, "locked": <bool>}. Storage that is missing or throws leaves the pane at its default width for the session, and the server renders the default width. A stored width wider than the screen allows is capped so the main column keeps the pane's minimum, and is written again only when the reader moves the edge.
  • RefusalNotice draws a write the server refused as a warning Alert: the title and sentence the page passes, an optional way forward, and a link offered on every refusal. The way forward is a secondary button and the other link a ghost, or the way forward is the primary with lead. A way forward on another origin opens in a new tab.
  • GateNotice draws what a page shows in place of a read that was refused or failed, for each state of the exported Gate type: absent, locked, full, forbidden, busy, unavailable and error. Every sentence comes from the words prop, the locked state renders the page's locked snippet with the link the gate carried, and busy, unavailable and error offer a link to retryHref that asks for a fresh document. A read that answered draws nothing.

Fixed

  • FileInput names the accepted file types in a short line. The drop zone printed its accept list as written, so .csv,.json,application/json ran as one long line of extensions and media types. Each entry now folds to the name a reader knows the format by, duplicates collapse, and the names join with "or", so that list reads "CSV or JSON". The input still receives the raw list, and a vendor type with no short form stays as written.
  • The package lists its stylesheets as side effects. It declared itself free of side effects, so webpack dropped a bare import of the stylesheet and the theme tokens never reached the page. The JavaScript modules still tree shake.

0.40.0 2026-10-16

Added

  • Panel takes tone="danger" for a group of destructive actions, such as deleting an account. It draws an opaque danger-tinted box with a danger border and puts the heading and its icon in the danger color. It is not a live region, so it can hold a form, which Alert and Banner cannot. With variant="plain" there is no box and only the heading carries the tone. The default tone="neutral" renders as before.

Fixed

  • The notes on Field's group prop and on SegmentedControl's labeledBy and labelHidden say what the components do. A SegmentedControl named by a Field caption takes labelHidden as well, or it draws its own caption under that one. FilterChips takes no labeledBy: it names its group from its own label, so give that label the caption's words. The group note had named FilterChips among the controls that take labeledBy, so a caller who followed it got a type error. Nothing renders differently.
  • The notes for 0.39.0 left out one case of the generated id. An Input, Textarea, NumberInput or SearchInput given neither a label nor an id has carried a generated id since 0.39.0, where 0.38.5 gave it none, so a test that expected no id on such a field now finds one. An Input, Textarea or NumberInput among them has also pointed aria-describedby at its hint or error since that release. SearchInput takes no hint or error.
  • A page transition no longer cross-fades for a reader who asked for reduced motion. The View Transitions API animates pseudo-elements that the stylesheet's reduced-motion rule did not match, so a page using document.startViewTransition or @view-transition kept its full fade. Those pseudo-elements now finish in 1ms like every other animation, in a rule of their own, so a browser without view transitions keeps the element rule.

0.39.1 2026-10-15

Fixed

  • The property that names a control after its Field caption is now spelled labeledBy, in US English like every other name in the kit, on SegmentedControl and in the wiring a Field given group hands its control. 0.39.0 spelled it with a double l. Rename it where you pass it or read it.

0.39.0 2026-10-15

Added

  • A Field given group draws its caption as text and hands its id to the control as labelledBy, and a SegmentedControl given labelledBy takes its name from that caption. A Field caption used to be a label pointing at the group, which a label cannot reach, so a click on it did nothing. The caption looks as it did, and a Field without group is unchanged. <!-- us-english: keep: the name 0.39.0 shipped -->

Changed

  • An Input, Textarea, NumberInput, SearchInput or Select given no id now takes a unique generated id instead of one made from its label. Two fields with one label on a page no longer share an id, so each label names its own control and each control reads its own hint or error. A page that pointed a Label's for or a selector at the label-made id now passes id itself.

Fixed

  • A list or a calendar opened in a container too short for it on either side, such as a one-field Modal, opens at its own height below its field. The container grows to hold it, or scrolls down to it when it cannot grow. A DatePicker there opened 34px tall and showed only its month row, and a Select, MultiSelect, Autocomplete or Dropdown showed no whole row. A list now keeps at least four rows in view, or all of them when it has fewer, and a calendar keeps its whole grid.
  • Under a finger, each NumberInput stepper's 44px target meets the number and grows outward, in either text direction. Centered on the 32px stepper, it reached 6px into the number, so a tap just inside the field stepped the value. A mouse sees it as it was.
  • Under a finger, the buttons in a CopyField sit 17px apart, so the 44px targets of Show and Copy meet instead of overlapping by 15px, and a tap on the end of Show no longer copies the value. A mouse keeps the 2px spacing.
  • Under a finger, the logo link at the top of an AuthShell takes a 44px target. It was 36px tall. A mouse sees it where it was.
  • Under a finger, the remove crosses on the top and bottom rows of MultiSelect chips keep their 44px targets inside the field. They reached 5px past its top and bottom borders, so a tap just above or below the field removed a value. The field pads its chips 11px from its border there, and one row of chips still fills the 44px control height. A mouse sees it as it was.
  • The kit draws its own icons on the five-size ladder the consoles use. The show and hide eye in a PasswordInput and a CopyField, and the copy icon beside it, are 14px to match the field's text, and were 15px. Their buttons keep their size, so no touch target moves. The AppShell sidebar toggle is 20px, as the menu button it replaces on a phone is, and was 18px.

0.38.5 2026-10-15

Fixed

  • Under a finger, the remove cross on each MultiSelect chip and on a removable Tag keeps its 44px target off its neighbors. The chips in a MultiSelect sit 8px apart there and their rows 24px apart, and a removable Tag holds that room around itself, so a tap meant to open the list or to reach the next tag no longer removes a value. Every Tag centers itself in its row, so a plain one beside a removable one neither stretches nor sits higher. A mouse sees both as they were.
  • A MultiSelect is drawn in the order Tab reaches it: the field first, then the remove button of each chip from the start of the row. The field used to be drawn after the chips while Tab reached it before them. Its chevron now sits centered at the end of the field however many rows the chips take, and a long placeholder is cut short rather than wrapped.
  • A list or a calendar wider than the room it opens in shrinks to that room. Under a finger the DatePicker calendar is 320px, so inside a Modal on a 320px phone it ran past the modal body, which then scrolled sideways. Where there is room it keeps its width. In a calendar shrunk that way, each day's target takes the width of its column and keeps its 44px height, so neighboring days' targets still meet without overlapping. A calendar never shrinks below the width of its days, 262px or 250px under a finger, so in a narrower container it keeps its start edge in view instead of scrolling sideways inside itself. Under a finger the previous and next month buttons keep their 44px targets inside the calendar, which used to pan about 3px sideways around them.
  • A TreeView branch unmounts its rows once its collapse has played. A branch that had been opened once kept its rows mounted for good, so a large tree that a reader opened and shut held every row it had ever shown.
  • In a right-to-left page, a Drawer slides in from the edge it is docked on and back out, as the AppShell navigation drawer does. A drawer docked on the left slid in from the right across the whole screen.

0.38.4 2026-10-15

Fixed

  • Under a finger, the options of a CheckboxGroup or a RadioGroup sit far enough apart that each takes a 44px target of its own, in a column and in a row that wraps, so a tap near the bottom of one option no longer picks the option below it. Card options keep their spacing, and a mouse sees the list as it was.
  • Under a finger, every day in a DatePicker or DateTimePicker calendar takes a 44px target of its own. The calendar opens 48px wider there, 320px, which a phone of that width holds edge to edge, and its weeks sit 12px apart, so a tap near the edge of a day no longer picks the day beside or below it. Any list or calendar that would run past the side of the screen, or of the dialog or scrolling box it opens in, now slides back inside it. A mouse sees the calendar as it was.
  • Under a finger, every option in an open Select, MultiSelect or Autocomplete list is 44px tall, as a Dropdown menu item already was. The rows were 36px. A mouse sees the list as it was.
  • Under a finger, every TreeView row is 44px tall, the height a SidebarNav row takes there. The rows were 32px. A mouse sees the tree as it was.
  • Under a finger, the close cross on a dialog opened with openDialog takes a 44px target, as the crosses on Modal and Drawer do. It was 32px. The cross is drawn the same size, and a mouse sees it as it was.
  • Under a finger, the cross that clears an Autocomplete takes a 44px target, as the one in a SearchInput does. It was 24px. A mouse sees it as it was.
  • A FileInput reached with the keyboard draws the 2px brand focus ring around its drop target. Focus used to land on the hidden input and show nothing at all. A click still opens the file picker with no ring.
  • A NumberInput, a MultiSelect and the search box inside an open Select or MultiSelect list draw the 2px brand focus ring under the keyboard, as Input and Textarea do. Focus used to show only as a 1px change in the border color.
  • Avatar initials read at 4.5:1 or better on every one of its eight colors in both themes. Seven of the eight read lower in one theme or the other, down to 1.87:1 in light, because dark initials sat on a see-through color that took on whatever the avatar was drawn over. The five strong colors are now solid behind the same initials and the three soft ones a light tint behind body-colored initials, and each name keeps its color.
  • The count on the active Tabs tab reads at 4.5:1 or better in both themes, at least 5.42:1. Its tint was four times as strong as every other brand chip's, and the count read as low as 3.28:1. It now takes the same light tint as the rest.
  • A colored SidebarNav badge and the colored initials in an AccountMenu read at 4.5:1 or better on every row, at least 4.68:1. On the current page's row, and on any row under the pointer or a finger, the row's own highlight showed through the badge and a violet badge read as low as 3.69:1. A badge on a resting row looks as it did.
  • The warning sign in a confirm dialog is sized by its icon size, 20px, rather than by a class. It looks as it did.
  • On a phone, the AppShell navigation drawer slides in from the edge it is docked on and back out, the right edge in a right-to-left page, and the dimmed page behind it fades in and out, as a Drawer does. Both used to appear and vanish in a single frame.
  • A Banner that is dismissed, by hand or by autoDismiss, fades away as an Alert does. It used to vanish in a single frame.
  • A screen reader announces a danger or warn Banner at once, as it does an Alert of the same tone, and waits for a pause for every other tone. Every banner used to wait, so "Payment failed" came after whatever was being read. A banner with autoDismiss still waits, whatever its tone.
  • A TreeView branch slides open and shut with its chevron, the way an Accordion panel does. Its rows used to appear and vanish in a single frame. A branch nobody has opened still renders nothing under it, so a large tree costs no more than it did.
  • A screen reader meets a MultiSelect as one field that reads its label and then the chosen values, followed by a remove button for each value, and Tab reaches the field before any remove button. The remove buttons used to sit inside the field's own control, a control nested in a control. After a value is removed, focus returns to the field, and a disabled field removes nothing and leaves its remove buttons out of the Tab order. A press anywhere on the field still opens the list, and the field looks as it did.
  • Under a finger, the check box in a checkable TreeView row takes a 44px square of its own beside the label, so a tap that means to check a row no longer opens or selects it instead. The rest of the row still selects it, and a mouse sees the tree as it was.
  • A danger or warn Alert given autoDismiss waits for a pause before a screen reader announces it, as every other tone does. It used to interrupt the reader and then clear itself, leaving nothing to act on. Without autoDismiss a danger or warn alert still interrupts.

0.38.3 2026-10-14

Fixed

  • Under a finger, each TimePicker segment takes a 44px target of its own, the hour, the minute, the second and AM or PM alike. The segments spread a few pixels apart there so no target takes its neighbor's, and a press anywhere else in the field, on its padding or a colon, focuses the nearest segment, as a native time input does, under a mouse as well. A mouse sees the field as it was, and the keys, the names a screen reader reads and the value are unchanged.

0.38.2 2026-10-14

Fixed

  • Under a finger, FilterChips that wrap to a second row keep a 44px target on every chip. The rows sit 18px apart, so a tap near the bottom of a chip no longer lands on the chip below it. A mouse keeps the 6px gap.
  • Under a finger, each SegmentedControl segment takes a 44px target, at least 44px wide for a short label such as "6h" and 44px tall inside a row that scrolls sideways. The track draws its frame as an inset outline with no border there, so the scroll box no longer cuts a pixel off the top and the bottom of every target, and a contrast theme keeps the frame. A mouse keeps the border and the widths it had.
  • Under a finger, Pagination page numbers sit 16px apart, so each one takes a 44px target of its own instead of 30px. On a phone held by a finger the pager shows the first page, the current page and the last, with a gap mark for each run between them, so it stays on one row. From the sm width up, and under a mouse, it shows the pages it did before.

0.38.1 2026-10-14

Fixed

  • A PageShell title with no spaces, such as a tenant slug, breaks inside the page width. It used to run past the right edge and widen the page, so on a phone the whole page panned sideways. A title with spaces still wraps at them.
  • A long FileInput accept list wraps inside the drop target. A list with no spaces used to print as one line that spilled out of both sides of the dashed box. The input's accept attribute is unchanged.

0.38.0 2026-10-14

Added

  • SegmentedControl and FilterChips take id, hint, error and describedBy, the field props Input, Select and RadioGroup take. The hint or the error shows under the row, and the group's aria-describedby points at it and then at the ids in describedBy, so a row inside a Field reads that field's message. A segmented row of radios carries aria-invalid while it shows an error. Both link modes take the same props, and a row given none of them looks and reads as it did.
  • Both rows now take a number from Svelte's id counter even when no new prop is set, so a generated id rendered after a SegmentedControl or a FilterChips on the same page is renumbered.
  • Input, Select and RadioGroup take describedBy, ids read after the control's own hint or error, and RadioGroup takes id for its fieldset. A control given neither looks and reads as it did.
  • CopyButton takes title, a tooltip shown on hover and on keyboard focus. The button keeps its name, its visible text or its label on an icon alone, and a title that says more than that name becomes its accessible description.
  • CopyField takes wrap, which shows the whole value across as many lines as it takes instead of one truncated line. It breaks anywhere, since a key or a token has no spaces to break at. A masked secret stays on one line and wraps once revealed. The default is still one truncated line.
  • EmptyState takes level, 2 or 3, the heading level of its title, named as SectionHeading names it. It stays an h3 by default. A page whose only other heading is its h1 passes 2, so a screen reader does not meet a jump from h1 to h3.

0.37.0 2026-10-13

Added

  • FilterChips takes an href on each chip, the link mode SegmentedControl already has. Every chip carries one or none does, the current chip is marked aria-current="page", and a link row works before the page hydrates, so a filter held in the URL can offer more options than a segmented control holds.
  • CopyState and copyToClipboard are exported, with the CopyStatus type, so an app builds its own copy control on the kit's clipboard write and its confirmation timing. CopyButton takes text, a visible label beside its icon, which also becomes its accessible name.
  • setDocumentTitleFormat lets an app state its browser tab title format once, in its root layout. Every PageShell below it then titles the tab from its title, or from documentTitle when the tab should read differently. With no format stated, PageShell renders no title element, so an app that renders its own keeps it.
  • AuthShell takes a lockup snippet in place of the product mark, and passes class and style to its outer element, so an app can show a tenant's logo and scope a tenant's accent to the sign-in frame.
  • The StatusTone and AccentTone types are exported.

Fixed

  • Under a finger, every SegmentedControl segment and both NumberInput steppers take a 44px target, and a small segmented track grows to the control height. A number field keeps 64px for its value inside a w-32. Nothing moves under a mouse.
  • The collapsed SidebarNav rail stacks a group row's disclosure under its link. Side by side the two did not fit the 56px rail. Under a mouse the link shrank to 18px with its icon under the chevron and the rail scrolled sideways, and under a finger it scrolled further.

0.36.0 2026-10-12

Added

  • Button takes size="icon", a square for a button whose only content is an icon. The icon sets the size and the box adds 8px round it, and a coarse pointer grows it to the control height like every button. Name it with aria-label, which also becomes its hint on hover.

Changed

  • ThemeToggle draws itself as a ghost Button at the icon size, so a control set beside it takes the same box by naming the size rather than copying its classes, and its name shows on hover as well as to a screen reader.

0.35.2 2026-10-12

Fixed

  • Autocomplete posts the chosen option's value. Given a name, it put the name on the visible input, which holds the option's label or the words being typed, so a form posted "Alpha" where the option was "a". The name now goes on a hidden input that holds the value, the way Select posts.
  • The motion presets keep an element's own transform. Under animate:motion.reorder, a leaving list item is held where it stood by an inline translate, and an exit that animated transform replaced it, so the item jumped to the corner of the list as it faded. Every frame now draws the element's inline transform first, and <li transition:motion.toast animate:motion.reorder> leaves from its place.

0.35.1 2026-10-07

Fixed

  • Checkbox keeps a tick made before the page hydrates. The box wrote its checked prop over the input as the page hydrated, so a box ticked while the page was still loading came back empty, and a form waiting on it stayed disabled. It now binds checked two ways and adopts the input's live state.

0.35.0 2026-10-06

Added

  • FilterChips, one filter out of many: a wrapping row of pressable chips, each with an optional count and icon, one always pressed. For a filter with more options than a SegmentedControl holds.

Changed

  • PluginCart draws its category row with FilterChips.

0.34.0 2026-10-06

Changed

  • Breaking: Select always draws the kit's listbox. The native select is gone, because the browser draws its option list in its own font and colors, which no theme reaches. mode, children (hand-written option elements), onchange and the SelectChangeEvent type are removed. Pass options as data and read the value through bind:value or onvaluechange, which now fires once the hidden input holds the new value, so a form can be submitted from it.
  • TimePicker draws its AM and PM part as a spinbutton like its other segments. A click, an arrow key, or A or P sets it.

0.33.1 2026-10-03

Fixed

  • PluginCart wraps a long one-word plugin name beside its price instead of running it under the price.

0.33.0 2026-10-02

Added

  • PluginCart takes stickyTop, how far below the top of the window the summary sticks, so a page with a sticky header keeps it in sight.
  • CartPlugin.maturity, the catalog's maturity. A card shows a Beta badge when it is "beta". Until a catalog sends it, betaOf reads a "Beta." sentence or a "Beta:" caveat from the blurb.

Changed

  • PluginCart cards put the price beside the name, the category and a Beta badge on a row of their own and the description alone below, and every card in a row matches the tallest. A screen reader hears the price, the description and the badges with each card.
  • Categories are wrapping chips with their counts, in place of a segmented control that scrolled sideways and hid the last categories.
  • Quick picks are labeled "Start from" and drawn as buttons. The summary draws the volume ladder as a meter, and on a narrow screen a bar under the cards keeps the count and the total in view and links to the summary.

0.32.1 2026-10-01

Fixed

  • AppShell positions its scrolling main region. An sr-only label low on a long page took the viewport as its containing block, escaped the region's clip and stretched the document past the shell, which gave the window a second scrollbar and scrolled the sidebar and header off screen.

0.32.0 2026-09-28

Added

  • PluginCart, the capability picker and its running summary, so the public price page and the customer portal stop shipping two carts. It renders a quote and never computes one: the total, the volume discount and the annual figure all arrive from the server, and a quote that no longer describes what is selected is withheld rather than shown. Its checkboxes carry a name and a value, so a host that wraps it in a form still posts the selection with no JavaScript running.
  • ladderState, ladderRows, billedCount and formatCents, the volume ladder as a reader sees it. Presentation over the brackets, never pricing.
  • CheckoutSteps, the same four labels on both of the sites a purchase crosses, so the hop between them stops reading as arriving somewhere else.

0.31.1 2026-09-26

Fixed

  • PasswordInput and SearchInput keep what was typed or autofilled before the page hydrated. Both set the field's value one way, so hydration wrote the empty model back over it, and a required password field then stopped its form from submitting.

0.31.0 2026-09-26

Added

  • CopyField: a read-only value with its copy control inside the field's end, for keys, secrets, hashes and addresses. secret masks the value and adds a reveal beside the copy; actions puts the caller's own controls in the same row. Focusing the field selects the whole value. It replaces a read-only Input with a separate outlined button beside it, which read as two controls of different heights.
  • watchThemePreference(onChange): hears every preference any control on the page sets, and one another tab stores, so a control showing the preference can follow a change made elsewhere.

Fixed

  • ThemeToggle follows a preference set by another control. A settings picker on the same page left the header toggle on its old value, and a toggle still holding system repainted the page to the OS theme at dusk over the reader's explicit choice. setThemePreference and setTheme now announce the change and the toggle listens.

Changed

  • CopyButton turns its icon into a check in place, crossing the two over on the motion rungs instead of swapping them in one frame, and shares its clipboard handling and timing with CopyField.
  • Copying works on an insecure origin. Where the async clipboard is absent or refuses, both controls fall back to the legacy copy command inside the click, so a console reached over a LAN address without TLS still copies.

0.30.0 2026-09-25

Added

  • Input and Textarea take mono, which sets the control alone in monospace. A font-mono class reaches the field wrapper, so it turned the label and the hint monospace as well.

Changed

  • SegmentedControl marks the chosen segment in the brand tone with an inset ring. On the surface-2 track it read as a slightly lighter box, and in the dark theme the bold label was the only visible difference.

Fixed

  • Dropdown sizes its panel to its longest item, capped at max-w-xs, and items no longer wrap. The panel was positioned against the trigger's wrapper and shrank to its width, so a label past nine characters broke onto two lines.

0.29.0 2026-09-24

Changed

  • Logo draws the brand book lockup: a bold wordmark with the E in the brand accent, and the mark's faces in the brand book's colors for each theme, the same lockup the documentation and marketing sites ship. The mark used to borrow the text ramp, which darkens for contrast in the light theme and put the top face darker than the front.
  • The mark paints from its own tokens: --color-mark-ink, -foot, -step, -top, -side, -face and -accent. A host that overrides --color-brand no longer recolors the mark; override the --color-mark-* tokens instead.
  • The wordmark is announced once as "LyEve" rather than in fragments.

Fixed

  • Under forced colors the mark's neutral faces take CanvasText and GrayText, so the column no longer disappears on a high-contrast canvas.
  • Printing keeps the light-theme faces, so the column is not white on paper.

0.28.0 2026-09-23

Added

  • Banner takes autoDismiss, on the same terms as Alert: a confirmation clears itself after five seconds and carries a close button, the timer holds while somebody is reading it, and a warning or a failure stays until dismissed. Which of the two components a surface reached for decided whether its confirmation ever left the screen.

0.27.0 2026-09-23

Added

  • Alert takes autoDismiss. A confirmation clears itself after five seconds and carries a close button, so nobody has to wait for it. The timer holds while the pointer is over the alert or focus is inside it. A warning and a failure are unaffected: they stay until dismissed, because the reader has to act on them.

Changed

  • Alert picks its live region from its tone. A failure and a warning take role="alert" and interrupt; a confirmation, a neutral note and a brand note take role="status" and wait for a pause. An assertive region that then removes itself is the worst of both.
  • AppShell eases the sidebar between the rail and the expanded column, and eases the rail's elevation with it. Both changed in a single frame before.

Fixed

  • An icon inside a Badge took a line of its own, so a pill carrying a count and a glyph rendered two rows tall. Preflight makes every svg a block and the badge's label is a block, which is what gives it its ellipsis; the icon is inline again and sits on the text's optical center.
  • Toggle carries the data-field marker. Without it the fit that sizes an overlay to the form inside it could not see a toggle, and six drawers in the consuming applications opened a rung too narrow. The suite that checks for the marker now finds a control that is its own label, which is the shape it was missing.

0.26.0 2026-09-22

Added

  • One width ladder for every surface lifted off the page, and a size that measures instead of guessing. Modal, Drawer and the dialog stack kept three ladders and none agreed, so the same form read at 672px, 384px or 512px depending on which surface a page opened it in. The five rungs are --container-overlay-sm|md|lg|xl|full (448, 576, 704, 896, 1088), each wider than the widest rung it replaces. Modal and Drawer default to size="auto", which takes md up to four fields, lg past four and xl past eight; the count is read from the data-field marker every labeled control now carries, so a radio group counts once and a form that reveals fields while it is open is re-measured. A caller that names a rung keeps it.
  • AuthShell, the frame for a page the app shell does not wrap: sign in, sign up, a password reset, an invitation, first-run setup. A full-height main on the ink token, a centered column at md (a form) or lg (a walkthrough), the lockup, one h1 at one size, an optional description, the children on a card, an optional actions row above (a theme toggle) and an optional footer line under the card (the one link off the page). Every console hand-wrote this frame and no two agreed: three heading sizes, a lockup on some pages, a theme control on others, and one class that named no token at all. AppShell states the signed-in frame once; this states the signed-out one.

Changed

  • The page caps grew and became tokens: --container-page-narrow 896px, --container-page-default 1152px and --container-page-wide 1536px, up from Tailwind's 768, 1024 and 1280. PageShell reads them by name, so a console retunes the cap for every page at once rather than per page.

0.25.1 2026-09-15

Fixed

  • An overlay's exit now plays when the page removes the component, not only when the component closes itself. The presets were local Svelte transitions, which play only when their own block toggles, and every console wraps a Modal in a page-level {#if} so the form inside starts fresh each time; on those pages the dialog still snapped shut. Every preset directive in the kit is |global now, and the consistency suite refuses one that is not. A page's own surface on a preset needs the same modifier: transition:motion.popover|global.

0.25.0 2026-09-15

Added

  • AppShell takes headerHidden, bindable, which puts the header bar away above md: for a page that owns the viewport, such as an editor in a focus mode. Below md: the bar stays whatever the prop says, because it carries the hamburger and that is the only way into the drawer. A page that hides the bar keeps its own way back on screen, since the shell's controls go with it.
  • PageShell takes titleHidden, which renders no title row at all and keeps the page's one h1 as sr-only. An editor whose toolbar is its header spent two rows before its canvas, the title row and then its own toolbar carrying the same name, with the shell's gap between them. A heading query and a screen reader still find exactly one h1; back renders nothing on such a page, so the page takes on the way back itself.
  • SegmentedControl takes href per option. When every option carries one, each segment renders as a plain anchor with no click handler, the chosen segment carries aria-current="page", and the group is a group rather than a radiogroup, so a page can keep its choice in the URL, survive a reload and work before any script has loaded. The arrows still move between segments, and the tab stop follows focus inside the group and returns to the current segment when focus leaves it. A row that mixes links and radios is refused with an error, since a radio beside two links would report a choice the URL never learns about. The value mode is unchanged.
  • One motion vocabulary, declared in styles.css beside the palette: four durations (--duration-fast, base, slow, progress) and three curves (--ease-enter, exit, move), each named for what it is used for. The transition-* utilities now default to the fast rung and the move curve, so transition-colors on its own is complete, and duration-base, duration-slow, duration-progress, ease-enter, ease-exit and ease-move are utilities for the rest. The values were never stated in one place before: three entrances ran at 120, 140 and 150ms on Tailwind's ease-out, and fifty class attributes said duration-150 to mean the default.
  • motion, a namespace of Svelte transitions that read those tokens at run time: dialog, scrim, drawer, popover, toast for a surface that mounts and unmounts, and reorder for animate: on a keyed list. An exit runs one rung faster than its entrance, on the exit curve. Every preset plays nothing for a reader who has asked for reduced motion, and nothing in a document with no Web Animations API, so a test that mounts and unmounts a surface sees what it always saw.
  • AppShell narrows the sidebar to a 56px icon rail between md: and lg:. At 768px the 224px column left the page 496px, and a flow editor with two docked panes had no canvas at all; --spacing-nav-rail existed and nothing used it. Under a pointer or with focus inside it the rail opens to the full column over the page, at the dropdown layer, and closes when either leaves, so the labels are one hover or one Tab away and the page keeps its width. The brand, nav and sidebarFooter snippets are told { rail }, so a SidebarNav takes it as collapsed and a brand row can drop its wordmark; a snippet written for the old contract still renders, clipped to the rail. A page can ask for the rail at every width above md: with rail, which is the focus mode the flow editor wanted; the drawer below md: is unchanged, and a collapsible shell that is collapsed still puts the sidebar away. SidebarState is exported for a caller that types the snippet by hand.
  • Every interactive primitive meets a finger at 44px and a mouse at the size it always had. Measured over 48 routes at 400px, 2,007 of 2,097 visible controls were under 44px and the kit owned 96% of them, because the control height is 38px and it is right at 38px on a desktop. The fix keys on pointer: coarse, so the desktop is unchanged to the pixel, and works two ways. --spacing-control is 44px under a coarse pointer, and the controls whose visual may grow (Button at every size, Tabs, SidebarNav rows and disclosures, ThemeToggle, AccountMenu, Collapsible, Dropdown items) state coarse:min-h-control, so a button and the input beside it grow together and stay level. The controls that are small on purpose keep their visual and grow an invisible hit box centered on themselves through the new hit-area utility: Toggle, Checkbox and Radio (on the label, which reaches the input), Breadcrumb links, PageShell's back link, Pagination, CopyButton, the close and dismiss crosses of Modal, Drawer, Alert, Banner, Toaster and Tag, the clear and reveal buttons inside SearchInput and PasswordInput, MultiSelect's chip remove, and DatePicker's days and month steps. The coarse: variant and the utility are declared in the theme file beside the tokens, so a consumer's build emits them and can use them on its own controls.

Changed

  • Every overlay now leaves the way it arrived. Modal, Drawer, the dialog stack and every toast played a CSS entrance and then vanished the instant their {#if} turned false, which read as a fault after the easing in. Dropdown, Select, MultiSelect, Autocomplete and DatePicker panels, which appeared with no motion at all, now unfold from the edge they hang off; Tooltip fades and settles; the AccountMenu panel plays the same frame on the way in. A SidebarNav group collapses its row to nothing the way an AccordionItem does instead of toggling display, and stays in the document, inert, so the disclosure's aria-controls keeps its target.
  • The dialog stack no longer waits a hand-written 200ms setTimeout before removing an entry: removing the entry plays the exit.

Fixed

  • A DatePicker's calendar, and so a DateTimePicker's, is no longer cut off inside a modal. The calendar sat under its trigger unconditionally, so a picker near the bottom of a modal body opened into the part of the scroll region nobody could reach and was cropped below the weekday row. The calendar is now the shared panel surface, placed the way every listbox panel is: it opens upward when the room below inside the nearest clipping ancestor is short and the room above is larger, it is re-measured on resize and on scroll, and inside a container too short for it on either side it scrolls rather than being cut. Its width moved from an arbitrary value onto the spacing scale; the calendar itself is unchanged.
  • Badge and Button labels never break inside a word. Neither carried whitespace-nowrap, so under a Table's default overflow-wrap: anywhere a status badge rendered as dra ft, a role as sup er_a dmi n, and a two-word ghost button in a flex row broke across two lines; one console marked 156 cells data-cell="nowrap" to stop it. A Badge also caps at its container's width and truncates with an ellipsis when its caller sets a narrower one; its label sits in a span of its own for that, because text-overflow does not reach into a flex item. A Button whose width is capped wraps its label in a truncate span itself, since the label and an icon before it are flex items and the button cannot tell them apart.
  • Table head cells align to the start. The table said text-start and the browser's own th { text-align: center } beat the inherited value, so every heading floated over a left-aligned column unless the page wrote text-left on each one. A column marked data-col="numeric" on its head and body cells ranges to the end in tabular figures instead. A sort button the page puts in a heading gets the same 44px hit box the kit's own small controls grow under a coarse pointer, through the table.
  • Tabs scroll sideways when the strip does not fit, and say so. Five tabs are 480px, and at 400px the fifth was past the edge with nothing on screen to say it existed; the workaround was a wrapped strip, which breaks the one line the underline runs along. The strip is a scroll box now, with the edge fades Table already draws on the side the tabs continue past, no scrollbar of its own, and the active tab scrolled into view when it changes. A tab never breaks inside its label, its focus ring is inset where the box cannot clip it, and the count pill is 12px, the last text on a measured page under that floor. The caller's class lands on the frame around the strip, so a margin does not scroll with it.
  • PageHeader and SectionHeading action slots wrap. Both were shrink-0, which sizes a flex item to its content and so cannot wrap even when the page puts a wrapping row inside it: four actions pushed New flow 83px past a 400px screen, and five filter chips ran 212px past it. The slot now sits beside the title while both fit, drops under it at the end edge when they do not, and wider than the row on its own wraps its controls inside itself with every line ending at the end edge, in the order given.

Removed

  • --duration-modal-in, --duration-toast-in, --duration-drawer-in and --duration-collapse, with the modal-in, drawer-in-* and toast-in keyframes. Nothing outside the kit read them; the presets replace them.

0.24.0 2026-09-15

Added

  • PageShell takes back, an { href, label } pair rendered above the title as an arrow and the parent page's name. A page two levels into a console had only the sidebar and the browser to get back with; a third of the nested pages in one app built a breadcrumb of their own, half of those drew an arrow by hand, and the rest offered nothing. The link is a plain anchor in the muted token, foreground on hover, with the kit's focus ring, and it shares the breadcrumb's row when both are given, the back link first, so a page with both does not stack two lines of navigation over its title. PageHeader is unchanged.

Fixed

  • A Toggle's off track reads as a control on a raised panel. The track was painted in surface-2 with no edge, and a form that sits on a surface-2 panel painted the panel the same shade, so until the switch was on only the knob showed, as a lone dot beside its label. The track now carries the same resting border every other control draws, which clears 3:1 against both surface levels in both themes; on, the border takes the fill's color, so the on state, the disabled state and the focus ring look as they did, and the knob's travel is unchanged.

0.23.3 2026-09-14

Fixed

  • A listbox row's active ring and hover tint stay inside the panel. The rows were square and full width inside a rounded, clipping surface, so on the first and last row the ring's corners were cut by the frame and the arc read unfinished. The list now insets its rows and each row carries a radius one step inside the surface's, so what a row paints has corners of its own; group headings and the empty line keep their text flush with the rows.
  • A panel opens upward when there is no room below. Select, MultiSelect, Autocomplete and Dropdown always hung their panel under the trigger, and nothing measured the space it needed, so a listbox near the bottom of a modal opened into the part of the modal body nobody could see without scrolling first. Each panel now measures its anchor against the nearest scrolling or clipping ancestor, or the viewport, on open, on resize and on scroll of that ancestor; it goes above only when it does not fit below and there is more room above, and its list is capped to the room on the chosen side. Dropdown takes the shared surface and scroll cap with it, so a long menu scrolls instead of running off the window.

0.23.2 2026-09-13

Fixed

  • A CheckboxGroup or RadioGroup legend keeps its distance from the first option. A rendered legend is not a flex item, so the fieldset's gap applied between the option list and the hint and never under the heading, and "Events" sat on its first row of boxes where an Input label sits 6px above its control. The legend now carries that 6px itself, the same distance a Field uses, and a hidden legend takes none.
  • A Table row's hover ring follows the frame's rounded corner. The ring was a box-shadow on the row, which takes no radius, so on the last row it drew square inside a rounded frame and the two bottom arcs sat bare outside it. The ring is drawn on the cells now, with the last row's outer cells rounded to the frame's inner radius so the shadow goes round the corner with it.

0.23.1 2026-09-13

Fixed

  • A tooltip is sized to its text and placed outside any scroller. The box was positioned inside an inline wrapper the size of its trigger, so the hint on an icon button took the button's width and broke one character per line, and a scrolling ancestor clipped it: a table's horizontal scroller clips vertically as well, so a hint on a row action was cut at the header row. The box is now fixed to the viewport and positioned from the trigger when it opens; a scroll anywhere closes it, since a fixed box cannot follow a trigger that scrolls out from under it.

0.23.0 2026-09-13

Added

  • An icon-only Button shows its name on hover and focus. The name was already there as aria-label, announced to a screen reader and shown to nobody else, so what a trash can or a toggle did was learned by pressing it. hint takes a string to say something other than the name, or false to keep the button silent; a full-width button stays full inside its hint, and a button with visible text and no aria-label renders as before.
  • Tooltip takes describe, on by default. Off, the text stays visual and the trigger gets no aria-describedby, for a trigger that already carries the same words as its name; with it on, a button named Delete was announced as "Delete, Delete".

0.22.2 2026-09-13

Fixed

  • A confirm dialog no longer renders the generic header above its own title. That header was an empty row holding only the close control, and the overlay put the first focus on that control, so a dialog asking "Delete?" opened with the ring on an X and Enter one Tab away from the danger button. The title now sits beside the warning icon, Cancel is the way out, and Escape still dismisses.
  • Focus lands on Cancel when a confirm dialog opens, so an Enter pressed before the dialog was read does nothing.

Added

  • The overlay honors data-initial-focus before falling back to the first focusable element, so a panel can name where focus lands.
  • confirm() takes a detail option, rendered on its own monospace line under the message. It holds the identifier a message used to carry mid-sentence, where a UUID wrapped.

0.22.1 2026-09-12

Fixed

  • A listbox row's active ring no longer draws past the rounded corners of the panel that holds it. The ring is square and the panel is not, so on the first or last row the corners poked out of the panel. The surface clips now, and Table takes the same clip on its frame so nothing a row draws can pass the frame's corners.
  • AppShell drops the browser's default outline on its landing region. The region is a target for the skip link, not a control, and the outline boxed the whole page body the moment the link had been used.

Changed

  • The declared Node floor is 24. Continuous integration has run on Node 24 for some time and the manifest still said 20, which described a runtime nothing was tested against. Node 22 consumers are no longer within the declared range.

0.22.0 2026-09-09

Changed

  • Nothing in the package. The release carried edits to the pull request template only. The entry that stood here repeated the 0.21.0 entry below.

0.21.0 2026-09-09

Added

  • Pagination takes count, the rows the page on screen actually holds, and states a range for a list with no total: jobs 51 to 100 rather than jobs, page 2. Without a total the component knew where a page started and not where it ended, because the last page is short and nothing told it how short, so an application whose endpoint reports no row count had to keep its own pager to say the sentence. count is read only when total is absent, and a page of no rows still names the page rather than printing an inverted range.

0.20.0 2026-09-09

Added

  • Pagination takes a noun and names it in the summary: files 51 to 100 of 4,210 instead of 51 to 100 of 4,210, jobs, page 3 instead of Page 3, and No webhooks instead of No results. A console that stacks several lists under one page title said which count belonged to which list nowhere. The word is plural at every size, because it labels the collection rather than agreeing with any figure in the sentence. A caller that passes no noun reads exactly as it did before.
  • The figures in that summary are grouped: of 4,210, not of 4210. The pager carries the largest number on the screen and sat under tables that already grouped their own, so one screen printed the same kind of number two ways. Grouping is en-US rather than the host's locale, because a server and the browser that hydrates its output need not report the same one and the figure would change under the reader. The page number of an uncounted list is grouped too, since href mode puts it in the URL and a link can land deep in a list nobody stepped through. The numbered page buttons stay ungrouped: they are a fixed square a separator does not fit.

0.19.0 2026-09-09

Changed

  • @lucide/svelte is a peer dependency instead of a dependency, at >=0.511.0 <2. It was the only runtime dependency the kit owned, and every application that consumes the kit depends on it directly too, so each resolved two copies: 42MB on disk in one of them, and eleven of the icons it imports shipped twice in that application's built bundle. Add @lucide/svelte alongside the kit if it is not already in your dependencies: a missing peer is resolved for you by pnpm and by npm, and not by yarn.

Added

  • Every component lays out with logical properties, so the kit mirrors under dir="rtl". It did not: the count was 69 physical directional utilities against 16 logical ones, and the visible end of it was a sidebar that stayed on the left in a right-to-left page. A unit gate holds the line, because the sweep is the easy half and the next component added would have started physical again.
  • Button, Pagination, SegmentedControl, Tabs, ThemeToggle and CopyButton acknowledge a press. hover: compiles inside @media (hover: hover), which is false on a finger, so a tap ran from rest to rest with the action already fired and the control never showed it had been touched. Every pressed step moves the same way on both ramps, so a press does not read as one gesture in one theme and its opposite in the other.
  • Table takes cell and truncateAt, and a single column overrides both through data-cell and the --cell-truncate custom property. One cell holding an unbroken token, an API key or a signed URL, pushed the table past its container and the columns after it left the screen. cell defaults to wrap, which breaks that token and does nothing else. truncate holds a cell to one line and keeps the whole value in its title.
  • Pagination renders links instead of buttons when given an href builder, and pages from hasNext when the endpoint states no total. A callback pager cannot run before the page hydrates, so paging a server-rendered list did nothing at all until the bundle arrived, and a list endpoint that reports no count could not drive the component. page, total, perPage and onchange behave exactly as before.
  • The console type scale is declared beside the brand ramp in styles.css: --text-xs through --text-2xl, each with its leading. The brand ramp is a marketing ramp with no step at 14px, which is the console's body size, so the components were built from sizes the token file never mentioned while five ramp tokens were referenced by nothing. Nothing renders at a different size for this.

Fixed

  • Button and Pagination drop an href whose scheme no component here will emit, and the rule lives in one place instead of two. The URL parser trims surrounding whitespace and strips tab, line feed and carriage return from anywhere in the value before it reads the scheme, so a tab written inside the word javascript still navigated while an anchored test on the raw string saw an unknown scheme and allowed it through.
  • The current row in SidebarNav answers the pointer like every other row. It was the only paint with no hover step, and it is the tail of the active trail, so on any nav deep enough to have sections it is a nested entry: beside siblings that all lit up, the row for the page you were on read as the one dead entry in the list.

0.18.0 2026-09-08

Added

  • Logo renders the canonical product mark. Every surface that needed it drew its own inline SVG, so the mark drifted between them and a change to it had no single place to land.
  • The theme control offers a system preference alongside light and dark. ThemePreference is 'light' | 'dark' | 'system', and getThemePreference, setThemePreference, resolveTheme, systemTheme, nextThemePreference and watchSystemTheme are exported with it. A visitor who has expressed no preference now follows the operating system, and keeps following it when the system flips, rather than staying pinned to whichever theme the first paint chose. getTheme, setTheme, toggleTheme and themeBootScript are unchanged.
  • Stacking order, motion and print are declared as tokens. Overlays, drawers, modals, dropdowns, tooltips, toasts and the skip link each name their layer (z-overlay, z-drawer, z-modal, z-dropdown, z-tooltip, z-toast, z-skip-link) instead of carrying a bare number. Seven components held z-50 and one held z-[60], so everything at one value fell back to DOM order and a tooltip could sit beneath the modal that opened it. Print is declared the same way: data-print="hide" on chrome, and data-print="unclip" where a scroll container would otherwise crop the printed page to a single screen.
  • Table fades the edge that has more content to show, so a table wider than its container says so instead of ending flush at the boundary.
  • confirm takes confirmLabel and cancelLabel through the new ConfirmOptions type. The dialog has always read them, but reaching them meant dropping to the lower level dialog API, so in practice every call site took the generic Confirm and no confirmation named the action it was confirming. A button reading Delete says what happens; one reading Confirm says only that something does.

Changed

  • PageHeader and SectionHeading sit on the brand type ramp. A --text-* token sets font size and nothing else, so both headings inherited whatever line height their parent happened to have. They now ask for the ramp's leading and tracking by name. The level 2 section heading stays at 16px, because the ramp holds nothing between 16px and 22px and 16px is the size of the card title it has to sit below.

Fixed

  • The skip link moves focus to the main region. It scrolled there and left focus behind, so the next Tab went back into the navigation the link exists to skip.
  • Dropdown implements the menu keyboard contract it declares. It took the ARIA menu role without the behavior that role promises: arrow keys, Home and End, type-ahead, and Escape returning focus to the trigger.
  • The icon-only controls in Autocomplete, MultiSelect and SearchInput meet a 24px target. The clear and remove affordances were drawn at the size of their glyph, which left them under the minimum on touch.
  • Table's scroll container is reachable by keyboard, and rows show a hover state. A region that scrolls could be scrolled only by pointer.
  • The exported VERSION agrees with package.json again. The 0.17.1 release bumped the manifest and the changelog and left the constant at 0.17.0. The two are checked against each other before the package is built, so the released tree did not build.

0.17.1 2026-09-07

Fixed

  • The package listed itself in its own dependencies, so the lockfile resolved @lyeve-labs/ui-kit from the registry as a dependency of @lyeve-labs/ui-kit. A dependency bump meant for a consumer of the kit had been applied to the kit. Nothing imported it. The supply-chain exemption that had been added to let the entry install is removed with it.

0.17.0 2026-09-07

Added

  • AppShell takes collapsible and a bindable collapsed, which put the sidebar away above md: and give the control that brings it back the hamburger's own square and gutter. Three products render this shell, and a page that owns the viewport, a canvas or a split pane, had 240px less than the viewport with no way to ask for it back. It is opt-in rather than on by default, because a new button in three headers at once is a decision each of those products makes for itself, and nothing here persists the state: where it is remembered is the app's decision, and the server cannot know, so the first paint is always the expanded one. Below md: the flag is ignored, since the same aside is the drawer there and honoring it would leave the hamburger opening nothing.

0.16.0 2026-09-06

Added

  • SectionHeading takes variant="eyebrow", the small uppercase label the consoles head a band of content with. It is a second treatment rather than a third level, because the element is a decision about document structure and the treatment is not: an eyebrow appears at both levels. The kit never named it, so thirty of them shipped hand rolled across two apps in four different bottom margins, and two bands on the same page sat different distances from their content. The heading carries no margin at either variant - the stack around it owns that distance.

0.15.0 2026-09-06

Added

  • AppShell, the authed application frame: the sidebar, the header bar and the content column. Three apps each hand rolled this and no two agreed. The sidebar was 224px in the admin and 240px in the customer portal and the ops console, opaque in two of them and 30% translucent in the third, built from SidebarNav in one and from inline anchors in the other two. Two of the three had no header at all above md:, so the page had no name on screen and no fixed place for the theme toggle or the account. Every one of those was a defensible local choice, and together they read as three products.
  • AccountMenu, the signed-in identity and the actions that belong to it, at the end of the header. Where it lives is the point: two apps put it in the bottom left corner of the sidebar and one put it in the header. The sidebar is also the worse of the two, because that column is already full height, so opening a menu in its last row pushes the last entry - Sign out, every time - past the bottom edge of the window. It is a native details, so the links and the sign-out post keep working on a page that never hydrated.
  • --spacing-header, the 56px shared by the header bar and the sidebar's brand row. The two meet at the top left corner and a 4px disagreement there reads as a broken seam.

Fixed

  • VERSION reported 0.13.1 from the 0.14.0 package. The constant was not bumped with the manifest, so a consumer reading the export to decide whether a fix had landed was told it had not.

0.14.0 2026-09-06

Changed

  • PageShell keeps the page gutter and the full-size title on a fill page. Owning the viewport is a statement about the content pane, so fill now drops only the content cap and the content gutter. It used to drop the title row's gutter too and render the heading through PageHeader compact, which made the pages that own the viewport the only ones in an app whose name sat at body size hard against the window edge: moving between one of those and any other page moved where the page began and changed how large its name was. The gutter composes from the same PAGE_PAD tokens, so the two titles now start at the same distance from the edge.

Added

  • PageShell takes compact, which drops the title to body size and hides the description. It was previously implied by fill and unreachable on its own, so a page with genuinely no room for a heading had to take the full-bleed content frame to get the small title, and a full-bleed page could not refuse it.

0.13.1 2026-09-06

Fixed

  • Controls keep a focus indicator. CONTROL_BASE and CONTROL_MULTILINE ended in outline-none and named no replacement, and a utility beats the base layer, so they canceled the global :focus-visible outline this theme declares for its contrast. Focus was left as a 1px border-color change on every text input, textarea, number and select in the kit. Both now pair the silenced outline with focus-visible:ring-2, which is what CONTROL_SEGMENT and SidebarNav's buttons already did.
  • SidebarNav no longer sets role="group" on a section's list. That role overrides the implicit list role, so every <li> inside sat under a parent that is not a list, which is an ARIA required-context error and a serious axe violation on every admin page that ships a section expanded. Grouping semantics belong to a tree, where the parent is a treeitem; this is a nav of plain links whose disclosure already declares itself through aria-expanded and aria-controls, so the role bought nothing and cost list semantics.

0.13.0 2026-09-04

The kit did not cover the controls the product needed, so every consuming page filled the gap with a raw HTML control. A native select opened in operating system chrome in the middle of a dark theme, a native date input opened the browser's own calendar, and twelve secret fields took an API key with no way to check what was pasted. This release adds the missing controls and, more importantly, the shared contracts that stop the ones already here from drifting apart again.

Added

  • PasswordInput, a text control with a reveal toggle. The button is a real type="button", so it cannot submit the form it sits in, and its accessible name states the action rather than the state.
  • TimePicker, with hour, minute and optional second segments in 12- or 24-hour display over a fixed 24-hour wire format. The segments are separate inputs rather than one masked field, because a masked input has a single accessible value and re-announces the whole string on every keystroke.
  • CheckboxGroup, the counterpart RadioGroup never had, and both now render a real fieldset and legend so the set is announced as a group.
  • SidebarNav, for navigation nested to any depth, with persisted expansion, a collapsed icon rail, aria-current on the active leaf and its ancestors, and an active rule under which a parent no longer claims its children.
  • TreeView, for nested data, implementing the WAI-ARIA tree pattern with roving tabindex and tri-state checkboxes whose branch state is always derived and never stored.
  • PageShell and SectionHeading. Nothing owned the page gutter, so pages re-declared it in four spellings, five content widths were in play with no rule behind the choice, and fourteen class strings served as a section heading. The gutter, the content cap, the centering and the section rhythm are now properties of the shell.
  • Checkbox and Radio take an icon, a size, a description, a card variant and a screen-reader-only label, spelled identically on both. Checkbox also takes indeterminate, which is bindable because the browser clears the DOM property on click and a one-way prop would silently desynchronize.
  • A richer Select. mode="listbox" opts into a custom panel with a search field, per-option icons and a custom trigger; mode="native" stays the default and is never inferred from passing options, because 28 of the 34 call sites sit inside a form and rely on the browser serializing a real form-associated element. The onchange event signature is frozen for the same reason: many call sites pass an unannotated arrow contextually typed from it, and one reads e.currentTarget.form.requestSubmit().
  • DateTimePicker, composing DatePicker and TimePicker under one label, one hint and one error, because they are one field.
  • Field, the field furniture as a component. internal/field.ts is private, so a consumer composing a control the kit does not ship had no legal way to match it and hand-copied the class strings instead.
  • FormMessage, for the outcome of a submit. A danger message interrupts with role="alert"; a confirmation does not.
  • CopyButton, SegmentedControl, Panel, DescriptionList, Toolbar and Collapsible, each replacing something the consuming applications had hand-rolled: a copy affordance with its own timing per page, a radio group drawn as buttons with the selection carried by color alone and no aria state, a lighter grouping that did not want to be a Card, key and value pairs laid out as div grids that convey no relationship, a filter row whose controls sat visibly misaligned, and a disclosure that owns only itself.
  • Card takes heading, headingLevel, icon and meta; Stat takes size, tone and mono. Two apps had each declared their own local Stat and the two were visually unrelated for the same job.
  • Seven internal contracts the components compose from, in the shape of the existing field contract: the option filter, the floating panel, the choice surface, the page layout, calendar and clock arithmetic, and the tri-state roll-up. FilterFn is public, so a caller can replace the matcher with its own without reaching into a private path.
  • --spacing-panel-max, --spacing-nav-indent, --spacing-nav-rail and --spacing-sidebar.

Fixed

  • MultiSelect and Autocomplete hand-rolled the popover, the keyboard model, the filter and the dismissal that the shared modules now own, and each copy carried the same defects. Autocomplete closed its panel on a 150ms blur timer, so selecting an option worked only because mousedown to click beat the timer. Neither set aria-activedescendant, so a screen reader was never told which row was active. Their option rows were in the tab sequence, so Tab walked into the list instead of leaving the field. Escape did not stop propagating, so one opened inside a modal closed both. MultiSelect's trigger and panel were unlinked in the accessibility tree.
  • A spread of ARIA attributes hides the role from the compiler, so it could not check aria-selected and aria-required against it. Three components stated the role only through a spread and the armed accessibility gate rejected them once it could finally see them.
  • The build's accessibility gate had never rejected anything. It tested startsWith('a11y-') and Svelte 5 renamed every warning code to snake_case, so no violation had matched for the whole life of the Svelte 5 port.
  • RadioGroup kept its focus ring inside the selected branch, so choosing an option removed the only indicator a keyboard user had. That is the defect Checkbox and Radio were fixed for one release earlier, still live one file away because nothing tested it. It also drew its resting border with line, which reads 1.25:1, and built its name from Math.random().
  • Autocomplete and RadioGroup generated instance ids from Math.random(), which differs between the server render and hydration, so every aria-controls and aria-describedby built from one pointed at an element that did not exist on the client.
  • The Playwright component suite had never run a single assertion. Its testDir pointed at a directory holding none of the sixteen specs, so every run collected zero tests and exited green.
  • check-dist could only ever reject a specifier beginning with @lyeve, so a component importing a relative path that the published tarball does not contain passed the check. It now resolves every relative specifier in dist against the file that declares it.
  • Coverage measured the wrong tree. A user-supplied exclude replaces the default list rather than extending it, so node_modules and dist were being measured.
  • NumberInput satisfied the field-contract guard while composing none of it, hand-spelling the border it was supposed to take from controlBorder.
  • FileInput drew its dropzone boundary with line at 1.25:1 and its hover with a fractional brand border, on the one element identifying it as a control.
  • Pagination rendered its elision marker as a literal ellipsis character with no aria-hidden, so a screen reader read "horizontal ellipsis" between page buttons.
  • PageHeader appended its bottom margin ahead of the consumer's class, so a page asking for a different gap shipped two competing margin utilities in one attribute and Tailwind's emitted order picked the winner rather than the page. The margin is now opt-out through flush, which PageShell sets because it owns the rhythm itself. It is opt-out rather than removed because 43 pages across the consuming applications render the component directly and take their heading gap from it; deleting it would have moved every one of them by 32px with nothing in their own source to explain why.

Changed

  • --spacing-card, --spacing-section and --spacing-page-y now carry their measured values. The three described a product that did not exist: card at 24px matched none of the card surfaces in use, section at 48px matched nothing anywhere, and no page rendered an asymmetric gutter. Composing from them is now a cleanup rather than a regression.
  • The public API guard compares the exported set against the documented list in both directions. It previously checked one direction against a hardcoded count, so a component added to the entry point and not to the list was exported, untested and invisible.

0.12.1 2026-09-03

Fixed

  • The published tarball carried a test file. files ships src/lib/styles wholesale, because the styles.css export points into it, and its test exclusions only cover dist. The contrast suite now sits beside the other library-wide suites in src/lib, which is not a published path.

0.12.0 2026-09-03

Accessibility pass across the library. The palette and the overlay components both carried defects that every consuming app inherited, and nothing measured either, so the same component was accessible in one app and not in another.

Added

  • --color-line-strong, for the boundary of a control rather than a divider. --color-line reads 1.25:1 against the surface, which is fine for a rule that carries no information and fails SC 1.4.11 for anything whose border is the only thing identifying it as a control. Inputs, selects, checkboxes, radios, toggles and bordered buttons use the new token; dividers keep --color-line.
  • Toggle accepts an id, so a <Label for> outside the component can name it.
  • A contrast suite that parses theme.css and measures every token against the grounds it is painted on, including inside a tint of itself, in both palettes.
  • Consistency guards for two mistakes that had already shipped: a utility class built from a runtime value, and an aria-modal surface with no focus handling and no accessible name.

Fixed

  • Modal and Drawer declared aria-modal="true" while leaving focus in the page behind them, with no focus trap, no initial focus, no focus restore and no scroll lock. A screen reader user was told a dialog had opened and then carried on reading the document underneath it. The behavior Dialog already had is now one shared action that all three use.
  • Modal had no accessible name and no height bound, so a dialog was announced as just "dialog" and content taller than the viewport could not be reached.
  • Dialog stacking generated no CSS. The z-index was written as z-[{zIndex}], and Tailwind matches complete class names in source text, so no rule was ever emitted and every stacked dialog rendered at z-index: auto.
  • confirm() rejected when the user canceled while documenting that it resolves false, so if (await confirm(...)) threw on the ordinary path.
  • A clickable Card took role="button" and tabindex from its onclick and then ignored Enter and Space.
  • Checkbox and Radio carried their focus ring only on the unchecked branch, so ticking a box removed the only indicator a keyboard user had.
  • Toasts were never announced. Each toast carried role="status" and arrived with its text already in it, which is not a change to a live region.
  • <Button href disabled> rendered a working link. disabled is not an anchor attribute and disabled:opacity-50 never matches an <a>.
  • Tooltip was unreachable by keyboard, because focus and blur do not bubble to the wrapper they were bound to; unreachable by screen reader, because nothing pointed at it; and could not be dismissed, which SC 1.4.13 requires.
  • Pagination rendered nothing at all for an empty list, so the "No results" string it carried was unreachable, and it wrote its range with an en dash.
  • Palette values below the AA floor. On the default dark palette danger read 2.85:1, so every form error in the library was unreadable, and violet 3.83:1 inside its own badge. On the light palette the focus ring read 2.39:1, and brand-light put near-white text at 2.45:1 on the primary button's hover state. faint, success and warn failed on surface-2.
  • The exported VERSION had drifted two releases behind package.json, so pnpm build failed its own version gate and the package could not be built.

0.11.4 2026-09-02

Fixed

  • The contributing guide named two private internal applications. This is one of the few repos that is actually public, and the list told a public reader what exists inside the consuming applications for no benefit.

0.11.3 2026-09-02

Fixed

  • The README sent maintainers to PUBLISHING.md for the release workflow. No such file exists; the workflow is in CONTRIBUTING.

Changed

  • CONTRIBUTING documents the branch model: work branches off dev and the PR goes back into dev, while main takes merges and carries the release tags.

0.11.2 2026-09-01

This release also carries the checkbox fix listed under 0.11.1. That version was written up and committed but never tagged or published, so the registry went from 0.11.0 to here and no consumer ever received it.

Fixed

  • The exported VERSION still read 0.11.0 while the package called itself 0.11.1. build runs version:check first, so the package could not be built or republished at all, and anything reading VERSION got the release before the one it was running.
  • A textarea threw away whatever was typed into it before the page finished hydrating. It rendered its value as element content, which compiles to a plain write of the server's value on the first client pass, so a reply typed during the wait was replaced by the empty string the server had sent. The field then failed its own required check and the submit did nothing. It binds the value now, which checks the element before writing and keeps what it finds. A rule test holds every textarea in the kit to that.

0.11.1 2026-08-31

Fixed

  • A click on a checkbox or radio landed on nothing. The visible box is a decorative element drawn over the real input, and it sat above it in the stacking order without forwarding the event, so the only reliable way to toggle one was the keyboard or a click that happened to land on the label.

0.11.0 2026-08-30

Fixed

  • The accordion panel snapped open and its text landed against the header. It now grows to the height of its own content over 200ms, and the body has room to breathe. The height comes from animating grid-template-rows between 0fr and 1fr, which resolves to the content's own height in CSS alone - nothing measures, nothing reflows per frame, and content of any size works.
  • A stray colored line under an open accordion header. It was the focus ring: the global :focus-visible outline sits two pixels *outside* the element, and the accordion clips its children, so three of its four edges were cropped and the fourth read as a rule. The accordion, the multi-select options and the toast dismiss button now draw an inset ring, which nothing can clip. Every component that clips and contains a button is checked for this.
  • The table's row transition had no duration and the table itself had a pointless one. [&_tbody_tr]:transition-colors duration-150 reads as one thought and is not: the bare duration lands on the element carrying the class. Both halves are scoped to the rows now.
  • The table header read as another body row. It has its own ground.

Added

  • The kit honors prefers-reduced-motion. Every animation ran regardless of what the reader asked for - the drawer slid, the toast flew in, the indicator's ping looped forever. Handled once in theme.css, so it covers components added later too. Durations are reduced rather than zeroed, so anything waiting on an animationend still fires.
  • The accordion wires its header to its panel: aria-controls, role="region", aria-labelledby, and inert on a closed panel so the tab order and a screen reader agree with what the eye sees.

Changed

  • A closed accordion panel stays in the DOM. It has to, for the panel to animate to its own height. It is collapsed to nothing and inert, so it is neither visible, focusable, nor announced - but a test asserting the body is *absent* when closed will now fail. Assert inert and the collapsed row instead.

0.10.1 2026-08-30

Fixed

  • homepage pointed at ui.lyeve.com, which has no DNS record and has never resolved. The docs site is served from ui-kit.lyeve.com. An audit noted the mismatch in July; the link has been dead on the npm package page since 0.7.0.

0.10.0 2026-08-30

One sweep, so that no component looks like it came from a different library than the one beside it. Two internal modules now hold what forty-eight components each spelled out for themselves, and a source-reading test suite fails when they drift apart again.

Added

  • label on Select, Textarea, NumberInput and SearchInput; hint on Select and NumberInput. Every field now offers the same set.
  • class on Button, Checkbox, Radio and Toggle. Button had accepted one only through its rest spread, where it replaced the computed class string instead of adding to it, so class="w-40" silently dropped every variant and size.
  • aria-invalid and aria-describedby on every field. Only Input carried either, so a screen reader announced the control and never the reason it was rejected.
  • neutral on Alert, matching Banner, and on Progress, matching the other accent components.
  • --spacing-control, the height of a single-line control, stated once.

Fixed

  • the kit shipped its palette tokens and none of the utility classes its own components are built from. Tailwind skips node_modules unless told otherwise, so it never read them. A component rendered only the classes its host app happened to use elsewhere in its own pages, which is why the same component looked different in two apps and why some of it looked unfinished in both. theme.css now declares @source, and every class a component needs comes from the kit. Expect the first build after upgrading to add a few kilobytes of CSS that should have been there all along.
  • a NumberInput sat two pixels short of the Input beside it: it was the one control whose height was a literal rather than derived from the same padding as the rest.
  • Select and Textarea moved their border to border-brand/50 on focus while the other seven fields moved to full strength, and showed border-danger/70 where the rest showed border-danger.
  • Select, Textarea and NumberInput spaced their label, control and message row a quarter-step tighter than the six fields around them.
  • Textarea rendered its hint underneath its error. Every field now replaces the hint with the error.
  • Checkbox was laid out flex where Radio and Toggle were inline-flex, so it stretched to fill its row while they did not.
  • Alert, Toaster, Modal, Drawer, Autocomplete, MultiSelect and NumberInput drew their close, check, warning and step icons as the literal characters x, v, ! and -, which take whatever weight the reader's font gives them. All icons are stroked SVG on one grid.
  • every color transition names its duration rather than inheriting one.

Changed

  • brand is the canonical name for the cyan tone. Alert, Banner and the toast store called it info; Badge, Tag, Indicator and Progress called the same color brand. info still resolves to brand, so toast.info(...) and tone="info" keep working - but toast.info now records the tone as brand, which is visible to anything reading toast.items[].tone.
  • Button transitions its colors rather than every animatable property.

0.9.4 2026-08-25

Fixed

  • avatar falls back to initials for a broken image under a script-src policy with no unsafe-inline, where the inline handler the server renderer emitted was blocked

Changed

0.9.2 and 0.9.3 were tagged but never reached the registry, so this release carries their changes as well. 0.9.1 is the last version consumers can install.

  • derive the exported VERSION constant from package.json; the build and the test suite fail when package.json, VERSION and this file disagree

0.9.3 2026-08-23

Fixed

  • brighten dark-palette success and warn tokens to meet AA contrast

0.9.2 2026-08-23

Fixed

  • raise the faint token above the WCAG AA contrast floor in both palettes

0.9.1 2026-08-11

Changed

Carries the 0.9.0 changes as well. 0.9.0 was tagged but never published, so this is the first release to reach the registry since 0.8.4.

  • Move to node 24 and pnpm 10.33.4.
  • Replace em-dashes in comments.

0.9.0 2026-08-06

Added

  • page-header: Name the page title for tests.

Changed

  • Apply prettier formatting.

0.8.4 2026-08-04

Fixed

  • Drop the unused Vite config that made svelte-package fail.
  • Stop shipping the Playwright component test suites in the published package.

0.8.3 2026-07-28

Published with no user-facing changes; repository tooling only.

0.8.2 2026-07-24

Fixed

  • Fix broken HTML example and dead external links in README.
  • Remove em dashes from README, CONTRIBUTING, CHANGELOG, and package.json.

0.8.1 2026-07-23

Fixed

  • Block javascript: and data: URIs on href props in Button and Breadcrumb.
  • Guard against null name, empty string, and missing accordion context in Avatar and AccordionItem.
  • Guard against NaN and Infinity in Progress and Pagination.
  • Add typeof document checks to dialog manager body-scroll functions so the module loads safely during SSR.
  • Set loading state before calling closeDialog in ConfirmDialog to avoid updating a component after it unmounts.
  • Add Escape key stopPropagation in Drawer and Modal so parent listeners don't also fire.
  • Fix Drawer enter animation direction for left-side variant.
  • Add blur handler to Autocomplete to close the option list on focus loss.

Changed

  • Replace hardcoded Tailwind colors with semantic design tokens in Dialog, ConfirmDialog, and Toggle.
  • Replace unsafe as type casts with runtime type guards in ConfirmDialog.
  • Narrow Record<string, X> to specific key unions in Avatar and Toggle.
  • Add explicit return types to toast store methods.
  • Add Promise<void> return types to Dialog dismiss and close handlers.
  • Replace .ts import extensions with .js in dialog components.
  • Remove redundant else branch in Dropdown $effect.
  • Add sideEffects: false to package.json so bundlers can tree-shake unused barrel exports.
  • Remove stale pnpm.overrides.cookie (no longer a transitive dependency).
  • Remove unused @types/node dev dependency.
  • Loosen @vitest/coverage-v8 from exact pin to ^2.1.9.
  • Remove dead build:docs CI step.
  • Remove legacy package entry from .prettierignore.
  • Remove @sveltejs/kit type annotation from svelte.config.js.

0.8.0 2026-07-23

Changed

  • Stripped SvelteKit app layer. Ui-kit is now a pure component library. Removed src/routes/ (docs site), @sveltejs/kit, @sveltejs/adapter-static, tailwindcss, @tailwindcss/vite, and vite. The npm package API is unchanged; consumers see the same dist/ output as before.

0.7.2 2026-07-21

Fixed

No runtime changes to shipped components.

  • Dropdown menu-item icon is a Svelte component prop; the icon test now passes a real Lucide component instead of a snippet, matching the documented type.
  • Suppressed a false-positive a11y_no_noninteractive_tabindex warning on the clickable Card. The element receives role="button" and a focusable tabindex together whenever onclick is set.
  • Dialog tests now consume the rejection from openDialog when a dialog is dismissed, eliminating unhandled promise rejections during the test run.

0.7.1 2026-07-20

Added

  • Additional component unit-test coverage (raised to 80%+).

0.7.0 2026-07-16

Added

  • EmptyState now accepts an iconSnippet prop, so consumers can render a custom icon component (e.g. a Lucide icon) in the icon chip instead of only a text/emoji icon string. iconSnippet takes precedence over icon when both are supplied.

0.6.2 2026-07-13

Changed

  • Apply prettier formatting to component test files.
  • Exclude generated CHANGELOG from prettier check.

0.6.1 2026-07-10

Changed

  • Pin pnpm 9.15.0 in mise to match packageManager + lockfile.
  • Add render tests for all primitives + logic coverage.

Fixed

  • Key Pagination ellipses by index to avoid each_key_duplicate.

0.6.0 2026-07-05

Added

  • Imperative dialog stack (openDialog/confirm), release v0.3.0.

Changed

  • Refine theme tokens.
  • Bump deps and pin cookie>=0.7.0 for CVE fix.
  • Sync pnpm-lock.yaml with package.json.
  • Apply prettier formatting to satisfy CI format gate.

0.5.0 2026-05-27

Added

  • Brand identity.

Changed

  • v0.2.0.

0.4.0 2026-05-23

Added

  • Interactive playground page.

Changed

  • README, CHANGELOG, CONTRIBUTING, MEMORY.
  • Add GitHub Actions workflow + PR template.
  • Green the install + check + test + package + docs-build pipeline.
  • Finish reference pages for the remaining 30 components.
  • Pre-publish polish.
  • Resolve styles.css to source for file: dev workflows.
  • PUBLISHING.md + publishConfig for the public npm push.
  • Pin Node 20 via mise to fix crypto.getRandomValues on build.
  • Ignore PUBLISHING.md and NOTES.md from git tracking.

0.3.0 2026-05-22

Added

  • Wire up the public API surface.
  • Scaffold docs site shell.
  • Landing page + getting-started + foundations.

Changed

  • Add reference pages for the launch set.

0.2.0 2026-05-21

Added

  • cn() class merger and theme helpers.
  • Toast service backed by Svelte 5 $state.
  • The 45-component primitive set.

0.1.0 2026-05-20

Added

  • Soft Dark + Soft Light token sheet.

Changed

  • Scaffold svelte-package + sveltekit project.