# @kungal/ui-core

## 2.58.0

### Minor Changes

- 81b3b71: New `KunCommandSearch`: a search field that stays in the top bar and opens KunCommandPalette's results in a panel under it. This is the top-bar search of Slack, Teams, GitHub, Jira and Steam. The kungal desktop app asked for it, and it was faking one with KunAutocomplete.

  - **Same content as the palette.** It takes the same `items` (flat or `KunCommandGroup[]`), `v-model:query`, `loading` and `highlight`, and renders the same rows: icon, section, label, description, match highlight, and the `#item` slot. Like the palette it does no searching: you compute `items` from the query and own the debounce.
  - **Opens on focus.** With an empty field it shows whatever `items` you pass, so recent searches go there. It opens nothing when there is nothing to show (no rows and no `#empty` slot), and `aria-expanded` follows what is on screen. Clearing the field keeps it open.
  - **Enter goes to the search page.** Unlike the palette, nothing is highlighted until an arrow key moves onto a row. Enter after typing emits `@submit(query)`; Enter on a highlighted row, or a click, emits `@select(item)`. The arrow keys cycle through "nothing highlighted", as a browser's address bar does. Every documented desktop app we checked behaves this way; none picks the first suggestion on Enter.
  - **Escape:** the first press closes the panel and keeps the text, the second clears the text. Tab, a click outside, selecting and submitting all close it. Focus never leaves the field: the panel is a listbox driven by `aria-activedescendant`, its options are out of the tab order, and there is no dialog, backdrop, scroll lock or focus trap.
  - **Size:** the panel is left-aligned 4px under the field, as wide as the field but at least 360px, capped to the viewport. Its height is the smaller of `maxHeight` (default 420) and the room below. `loading` turns the field's search icon into a spinner. Rows already on screen stay until new ones arrive, so the list does not blink on every keystroke.
  - **Shortcut:** ⌘K / Ctrl+K focuses the field. A field that is not on screen, or sits behind a modal, ignores the shortcut and leaves it to a KunCommandPalette on the same page. That is the narrow-screen recipe: show the field from `md` up and the palette's icon button below it with breakpoint classes, both fed the same `query` and `items`. The component never switches layout from JavaScript, so the server renders the right one at every width.

  **Changes to KunCommandPalette**, all additive:

  - `KunCommandItem.removable` adds a remove button to a row (recent searches, say). Shift+Delete removes the highlighted row; both emit the new `@remove`, you drop the item from `items`, and the highlight stays on the row that takes its place.
  - A labelled group is now `role="group"`, named by its heading. A group with no items is no longer rendered: it used to leave an orphan heading.
  - The palette's ⌘K ignores a keystroke another handler already consumed, which is how a visible KunCommandSearch takes it first.

  **KunModal and KunDrawer** no longer close on an Escape that a field inside them already used. A KunCommandSearch clearing its text and a KunChatComposer cancelling its reply bar each took the modal down on the same press.

  Locale: `commandPalette.remove` and `commandPalette.clear` (zh-CN and en), shared by both components.

## 2.57.1

## 2.57.0

## 2.56.2

## 2.56.1

## 2.56.0

### Minor Changes

- 570067b: Coloured text and placeholders now clear WCAG AA (4.5:1). A kungal Flutter app's contrast check measured KunUI's own coloured text and placeholders below it, and the web uses the same classes.

  **New tokens:**

  - `--color-{color}-text` (Tailwind `text-primary-text`, `text-danger-text`, …), one per semantic colour, `default` included.
  - In Dart: `KunColorScale.text`.

  **Why a new token.** A fill colour used as text measured 1.75–4.42:1 on the light page for every colour but `default`, and in dark mode primary and danger fell to 3.87:1 and 3.66:1 on cards. `{color}-text` is the ramp step closest to the fill that clears 4.5:1 on:

  - the page background;
  - cards (`content1`);
  - the colour's own 20% tint over each, which is the hover and keyboard-focus fill of a light item and the fill of a flat chip.

  The token generator measures all four in both modes and fails the build if one misses. The steps it picks:

  | Mode  | Step 600                      | Step 700                                |
  | ----- | ----------------------------- | --------------------------------------- |
  | Light | secondary, warning            | primary, success, danger, info, default |
  | Dark  | every colour except secondary | secondary                               |

  **What moved to it:**

  - The `light`, `bordered` and `flat` variants (buttons, chips, badges, menu items) and `kunTextClasses`, which covers KunLink, selected tabs, active steps and reactions.
  - KunInfo's `bordered` text, and its dark `flat` danger and info.
  - Every form error message and required asterisk.
  - Chat sender names, reply and pinned titles, links, mentions, typing and status text.

  **What you'll see.** Coloured text is one step darker in light mode and one step lighter in dark mode. Icons, SVG strokes, spinners and text on solid fills keep their colours.

  **If your own markup uses `text-primary` for readable text**, switch it to `text-primary-text`. `text-primary` stays the fill colour, meant for fills, borders and icons.

  **Placeholders** now use `foreground-muted`, the secondary-text colour: 5.52:1 light and 4.85:1 dark, where `default-400` measured 2.31–3.58:1. This covers the base-layer `::placeholder` rule (every native input), KunSelect's and KunDatePicker's empty trigger, KunChatComposer and KunCommandPalette. A placeholder is often the only label a field has, such as "全部平台" in a filter. Disabled text, outside-month days and icons keep their lighter steps: WCAG exempts disabled controls, and the others are not text.

  Measured on the docs site in Chromium: 227 text elements in each mode across the button, chip, tab, link, select, input, info, dropdown, chat bubble, date picker and textarea pages all clear 4.5:1. The lowest are a warning chip (4.77:1 light, 4.87:1 dark) and the dark placeholder (4.85:1).

## 2.55.0

### Minor Changes

- 0bac099: New `KunSplitPane`: two panes with a divider you can drag or move with the keyboard. It suits a list beside a detail view, or a main area with an auxiliary column. KunChatLayout now switches between one and two panes by its own width, and its list pane can be made draggable.

  **KunSplitPane** puts its panes in the `#start` and `#end` slots.

  - **Width.** `v-model:size` is the primary pane's width in px (default 360). It is a prop the server knows, so the server-rendered HTML already has the layout and the divider's `aria-valuenow`/`min`/`max`.
  - **Persisting the width.** `v-model:size` updates on every pointer move. Save from `resize-end`, which fires once per drag or key press, for example to a cookie your server reads back into `size`.
  - **Limits and snapping.** `min-size` and `max-size` default to 240 and 480. `snap-points` (e.g. `[360, 412]`) pull the divider in once it comes within `snap-threshold` px (default 8), and any width in between stays reachable. A keyboard step that ends within the threshold snaps too, but a snap never cancels or reverses a step, so a small `step` cannot get stuck on a snap point.
  - **Which pane is sized.** `primary="end"` gives the width to the right pane, for an auxiliary column.
  - **Keyboard** (WAI-ARIA window splitter pattern):
    - ← and → move the divider by `step` (default 10px); Shift moves five steps.
    - Home and End jump to the minimum and maximum.
  - **The divider** is a 1px line. Its grab area is 9px wide with a mouse and 21px on a touch screen, and it sits above the panes' content. A drag follows the pointer that started it; a second finger or the mouse cannot take it over. While you drag, the page keeps a col-resize cursor, no text gets selected, and the panes ignore the pointer, so an iframe can't swallow the drag.
  - **Narrow layouts.** Below `stack-below` (`md` = 48rem by default, `lg` = 64rem, or `false`), it shows one pane at a time, chosen by `show-pane`. This is a container query on the component's own width, not a media query on the window: a split nested beside a side rail or inside another pane stacks based on the room it actually has.
  - **Flutter port.** The snapping rule is a pure `resolveKunSplitSize` in `@kungal/ui-core`, with tests.

  **KunChatLayout** now shows two panes once the layout itself is 48rem (768px) wide, rather than once the window is.

  - **Why:** a chat beside an app's side rail used to switch to two panes in a 768px window that left it 679px, and the conversation was squeezed to 327px.
  - **What might change for you:** pages that give the chat less than 48rem (for example, a window of about 768–800px minus page padding) now show one pane.
  - **KunChatHeader's `back="mobile"`** follows the KunChatLayout around it. Outside a KunChatLayout it still uses the window's `md` width.
  - **New props:** `resizable`, `v-model:sidebar-size` (px), `sidebar-min-size` and `sidebar-max-size` make the list draggable through KunSplitPane. The `sidebar-resize-end` event is the one to persist from. Without `resizable`, `sidebar-width` works as before.

## 2.54.0

### Minor Changes

- 5ca083f: Menus gain separators, shortcut hints and one level of submenu, and a new `KunKbd` shows keyboard shortcuts. Everything is additive: existing `items` arrays and props render exactly as before.

  **KunContextMenu and KunDropdown** share one item list, so an app can render the same command list as a right-click menu, a ⋯ menu and a long-press menu. `items` now takes `KunMenuEntry[]`, which is an item or `{ type: 'separator' }`. `KunContextMenuItem[]` arrays are still accepted.

  - **Separators.** Leading, trailing and repeated separators are dropped, so a list filtered per context never shows a stray line.
  - **Shortcuts.** `shortcut: 'Mod+C'` shows ⌘C on Apple and Ctrl+C elsewhere at the item's end. It is display only; your app still binds the key. Screen readers read the item's label as its name and the shortcut as its description.
  - **Submenus.** `children` opens a submenu, one level deep.
    - Opening: hover (after 100 ms), →, Enter or a tap.
    - Closing: ← and Escape close one level, as the WAI-ARIA menu pattern specifies; Tab closes the whole menu.
    - A 300 ms grace period and a safe triangle keep the submenu open while the pointer moves diagonally towards it.
    - An item with `children` never emits `select`; if a filter leaves it no child items, it shows disabled. Selecting a submenu item emits that item and closes everything.
  - **Keyboard.** KunContextMenu now has the type-ahead KunDropdown already had. ↑ in a menu with nothing focused yet, such as right after a click opened it, now goes to the last item. Before, it went to the second-to-last.
  - **Link items.** Enter or Space on an `href` item now follows the link. Before, it emitted `select` and closed without navigating.

  **KunKbd** takes `keys` in the aria-keyshortcuts grammar plus `Mod`: `+` joins a chord and a space separates a sequence, e.g. `'Mod+K'`, `'Shift+Delete'`, `'G G'`.

  - **Order.** Modifiers sort into each platform's order: ⌃⌥⇧⌘ on Apple, Ctrl+Win+Alt+Shift elsewhere.
  - **Screen readers** hear key names ("Command K"), not glyphs.
  - **Size** is in em, so it follows the surrounding text.
  - **Variants.** `variant="plain"` is the one-line form menus use: ⇧⌘Z, or Ctrl+Shift+Z.
  - **Server rendering.** The server cannot know the visitor's keyboard, so it renders the Ctrl form; a Mac switches to ⌘ once the page has mounted. Components created afterwards, such as a menu or tooltip opening, start in the right form.
  - **Related APIs:**
    - `KunTooltip` gains `shortcut`: `text="Search" shortcut="Mod+K"` renders "Search Ctrl K".
    - `KunCommandPalette`'s `trigger` slot also passes `keys` for use with KunKbd.
    - `useKunShortcutPlatform()` and `formatKunShortcut()` are exported for your own markup.
    - The parser and formatter live in `@kungal/ui-core`, so the Flutter port renders the same labels.

  **KunChip and KunTagInput:** the × remove button now has a 24×24 px hit area. The glyph did not change size: it was 14 px in KunChip and 18 px in KunTagInput, which is under WCAG 2.2's 24 px minimum. The hit area is not clipped: in Chromium, a clipped one measured worse than none, because it switches off the browser's own tap adjustment.

  `useKunFloating`'s `offset` option now accepts floating-ui's full `OffsetOptions` (for example `{ mainAxis, crossAxis }`), not just a number.

## 2.53.0

## 2.52.1

## 2.52.0

### Minor Changes

- a56cbbf: Chat photos show the URL the server sends. NextMoe chat spec 1.1.0 (nextmoe-infra #337) gives every photo a `url`, which the spec describes as "where the image is served; never build it from image_hash". It is present on history, updates, pins, last messages, pushes and the upload response. `KunChatPhoto` gains `url?: string | null`. It is optional so payloads and fixtures from before 1.1.0 still type-check.

  `KunChatBubble`, `KunChatMessageList` (its lightbox included) and `KunChatPinnedBar` now show `media.url` when you pass no `resolve-media-url`, so a site on spec 1.1.0 can drop its hash-to-URL resolver. A resolver you do pass still takes precedence: keep it if you serve the bubble a smaller preview than the lightbox, because `url` is a single address for both. A photo with neither a `url` nor a resolver renders as before, with an empty `src`.

  A conversation's `photo_url` needs nothing new from KunUI. Pass it to `KunChatConversationItem`'s `avatar`, which already takes a URL.

## 2.51.1

### Patch Changes

- 70e5ec2: `normalizeKunChatEntities` is now a fixed point, as its documentation says. Inline entities were cut at the boundaries of every blockquote, including a quote that nesting then dropped because it sat inside, or crossed, another quote. A second pass had no reason to keep that cut and merged the pieces back together. For example, `'中中b*bab'` with a spoiler across two nested quotes normalized to three spoiler runs, and normalizing that result gave two. Blockquotes are now settled among themselves first, and only a quote that survives cuts anything. Output changes only for input with overlapping blockquotes. The property test that should have caught this drew its cases from an LCG computed in doubles, which cycled after about 10,000 draws. It now uses mulberry32, runs 20,000 rounds, and fails on the old code.

  `formatKunChatMarkdown` no longer loses text after a link or mention whose label ends on a line break. The parser checks for a `> ` quote prefix and the `\>` escape where the new line's text begins, which is after the label's closing `](…)`. The formatter decided whether to escape by reading the characters it had already written, and those end in `)` at that point. So `'a\n\\>b'` with a mention on the line break came back as `'a\n>b'`, and a `>` in that position was read as a quote marker and disappeared. The formatter now tracks the parser's line-start state. `parseKunChatMarkdown` is unchanged, so no composer input parses differently. Over 2.3 million random messages, `parse(format(m))` returned the text unchanged every time. The old code lost text in 50 of the first 300,000.

  `KunDatePicker` formats the trigger text in its locale. A custom `format` with a month or weekday token showed English names in a zh-CN app. Measured with `format="yyyy MMMM do EEEE"`: the trigger now reads "2026 九月 27 日 星期日" under zh-CN and "2026 September 27th Sunday" under en-US. The default numeric formats render as before.

  `KunDatePicker` shows which cell the arrow keys are on. Focus stays on the trigger while the arrows move `aria-activedescendant`, so no cell ever matched `:focus-visible`, and a sighted keyboard user saw nothing move until the page turned. The active cell now draws the picker's focus ring once a key opens the panel or moves the cell, and drops it on a pointer press inside the panel. This follows React Aria's `useOption`, which shows a virtually focused option as focus-visible only under keyboard input. Opening the picker with a click shows no ring until an arrow key is pressed.

## 2.51.0

### Minor Changes

- b4b6a8a: Chat components for the NextMoe private-chat service (`/v2/chat`), modelled on Telegram. Twelve new components take the API's JSON as it comes — snake_case fields and string ids included — so a site writes no conversion layer:

  - `KunChatMessageList` — the scrollback. Consecutive messages from one sender run together (avatar and tail on the last), day pills stick to the top, and the list opens at the "unread messages" divider. Older pages load above you without moving the view, new messages stick to the bottom when you are there and count on the scroll-down button when you are not, and `scrollToSeq(seq)` jumps to a message and flashes it. Right-click or long-press opens the message menu, and swiping left on a touch screen replies. Thousands of messages scroll smoothly with no virtualisation library: rows off screen skip layout and paint (`content-visibility`). The scroller is `column-reverse`, so the server-rendered HTML already sits at the newest message, and history growing above the viewport never writes `scrollTop` — an iOS momentum fling through it is not cut short.
  - `KunChatBubble` — one message: own (right, primary tint) or theirs; reply and partial-quote previews; a photo, or an album laid out with Telegram's mosaic (ported from Telegram Desktop via tweb); a cross-site context card; reactions; time, "edited" and delivery status (a failed message emits `retry`). A service message renders as a centred pill.
  - `KunChatText` — text + entities: bold, italic, underline, strikethrough, spoiler (one click reveals them all), inline code, a code block with its language and a copy button, quotes, links and mentions. There is no `v-html`: every run of text is a text node, and a link only renders for `http`, `https` and `mailto`.
  - `KunChatComposer` — the input. It grows with its content; Enter sends on a desktop and breaks the line on a touch screen. The composer shortcuts (`**bold**`, `||spoiler||`, ` ``` `…) are sent as entities. Reply, quote and edit bars are v-models, and editing puts the message back in the input and keeps the draft aside. Pasted or dropped photos are passed on as `attach(files)`, a counter shows near the 4096-character limit, and a throttled `typing` event fires while the user types.
  - `KunChatConversationItem`, `KunChatHeader`, `KunChatPinnedBar`, `KunChatRequestBar`, `KunChatMessageMenu`, `KunChatReactionPicker`, `KunChatTyping` and `KunChatLayout` (two panes from `md` up, one at a time on a phone), plus the `useKunChatTyping` composable. Typing notifications expire on their own after 6 s, so a site only forwards what it receives.

  `@kungal/ui-core` gains the wire types (`KunChatMessage`, `KunChatEntity`, `KunChatUser`, …) and the pure functions behind the components, for the Flutter port to reproduce: `normalizeKunChatEntities` / `buildKunChatEntityTree`, `parseKunChatMarkdown` / `formatKunChatMarkdown`, `layoutKunChatAlbum` and `groupKunChatMessages`. The two markdown functions are exact inverses — a message survives `parse(format(message))` — for everything the syntax can spell. The exceptions are listed on `formatKunChatMarkdown`: `url` entities, which the server detects again, and quotes that do not cover whole lines. Store a draft as the parsed `{ text, entities }` and restore it with `formatKunChatMarkdown`. These algorithms have the repository's first unit tests (`pnpm test`, `node:test` on the sources).

  What it costs you:

  - **Custom locales.** New locale namespaces (`chat`, `chatComposer`, `chatMenu`, `chatPinned`, `chatRequest`, `chatService`, `chatStatus`, `chatTyping`) ship in zh-CN and en. A locale you wrote yourself with `defineKunLocale` now fails to type-check until it has them.
  - **Icons.** 21 more lucide icons are bundled (message status, actions, the composer).
  - **Bundle size.** The components add about 23 kB gzip to `dist/index.js`, and they tree-shake away if you do not import them.
  - **Server rendering.** When you render chat on the server, pass `time-zone`: dates are formatted in the runtime's zone otherwise, and the server's and the reader's disagree.

  Photos need `resolve-media-url`: KunUI never builds an image URL from a hash. A chat avatar does not link to a profile — the chat ids are strings, while `KunUser.id` stays a number — so clicks come out as `user-click` for the site to route.

## 2.50.1

## 2.50.0

## 2.49.1

## 2.49.0

### Minor Changes

- f398a45: **`KunSteps` steps are clickable** when you bind `v-model:current`. Each step becomes a `<button>` whose hit area covers the whole step (indicator, title and description), and a click emits `update:current` with the step's index. With a plain `:current` nothing changes. The steps stay display-only, with the same layout as before, so a stepper that shows review status does not gain buttons that do nothing.

  - `linear` (default `true`) lets a click go back to any earlier step but never forward. Forward moves stay behind your own "next" button and its validation. This is MUI's linear rule. Reka UI also allows one step forward, which would skip that validation. `:linear="false"` makes every step reachable, for a guide whose steps do not depend on each other.
  - `items[].disabled` excludes one step from click navigation. It has no visual effect.
  - `items[].status: 'error'` marks a step failed (danger colour and an ✕), wherever it sits relative to `current`. One example is an application rejected at review.
  - Keyboard: every reachable step is a Tab stop, and Enter or Space selects it. There is no arrow-key navigation, and the list is not a `tablist`, because no panel exists for a step to control. KunTab draws the same line in its link mode, and React Spectrum's StepList, Mantine and Carbon make the same choice. Focus stays on the step you activated.
  - Screen readers now hear each step's state, in display-only mode too. A visually hidden prefix is read before the title: "Completed: …", "Current step: …", "Not started: …" or "Error: …". Before this change, a done step and a pending one sounded the same.
    - The strings are the new `steps` namespace in the locale catalogs. A custom `KunLocale` must add `steps.completed`, `steps.current`, `steps.pending` and `steps.error`, each taking `{title}`.
    - `kun_ui_messages` gains `KunStepsStrings` to match.
  - In display-only mode the indicator is now `aria-hidden`, because its number or icon repeated what the list position and the state prefix already say.

## 2.48.0

## 2.47.2

### Patch Changes

- 7e28622: `lucide:heart` is now bundled. It is `KunReaction`'s default `icon`, and an unbundled name renders nothing, so a reaction left on its default showed no glyph unless the site registered the heart itself. `kun_ui_icons` gains `KunIcons.heart` and `KunIcons.heartFilled`. The filled twin is Flutter-only: the web fills a stroke icon with `fill-current`, and a font glyph is a traced stroke that cannot be filled, so the generator traces a second glyph from the same SVG with its paths filled. The `FILLED` list in `icons-manifest.mjs` names the icons that get one.

## 2.47.1

### Patch Changes

- 6dc80fb: `KunScrollShadow` is now portable in the Flutter contract (`contracts/component-contracts.json`). Nothing changes on the web. The contract used to call it a pure-CSS answer to web scrollbars. The Flutter port now needs what it adds on top of a scroller: edge fades that show only while there is more content on that side, a vertical mouse wheel that scrolls a horizontal strip (`wheel`, with `'contain'` keeping the page still), and grab-to-scroll with a mouse (`draggable`). Flutter's `Scrollable` does none of these by default, and a Flutter app with horizontal shelves could not scroll them with a plain mouse wheel. Only `className` and `contentClass` stay web-only, as class pass-throughs do everywhere.

## 2.47.0

### Minor Changes

- cefb4b0: Seven new locale strings under `textSelection`: `copy`, `cut`, `paste`, `selectAll`, `share`, `lookUp` and `searchWeb`. They label the menu a text field shows over its selection, and only the Flutter port draws that menu: on the web the browser draws its own, so no web component renders them and nothing on a page changes. They are in the shared catalogs because `kun_ui_messages` is generated from the same source, and a string that exists on one platform only is what that pipeline exists to prevent. If you supply a full catalog of your own for another language, add the group; the wording follows Flutter's own `WidgetsLocalizations` for both built-in languages.

## 2.46.0

### Minor Changes

- 32c3e60: `KunBanner`, a full-width, one-sentence announcement strip for the top of a page: a maintenance window, a new release, an event. It takes `text` (or the default slot, where a link inherits the strip's colour and is underlined), an `icon`, `color`, a `solid` or `flat` `variant`, and a close button (`closable`, on by default). The `#actions` slot receives `{ dismiss }`, so a "Go vote" button can close the banner as well. Bind `v-model`, not `v-if`: closing collapses the strip over `--kun-dur-base` and the page below moves up with it instead of jumping 40px. The collapse is skipped under `prefers-reduced-motion`.

  `storageKey` keeps a closed banner closed after a reload. The dismissal goes to `localStorage` under `kun-banner:<key>`, and `v-model` set back to `true` clears it. Give each announcement its own key: a new key shows again to everyone who closed the old one. The server cannot read `localStorage`, so KunBanner renders a one-line inline script in front of the banner. It runs before the banner is parsed and hides a closed one through a custom property on `<html>`. Measured in Chromium on the server-rendered docs: a closed banner is already `display: none` when it is inserted, it is never painted, the layout shift is zero, and there are no hydration warnings in dev or production. When storage is blocked, every access fails quietly and the banner simply shows. Under a CSP that forbids inline scripts, pass `nonce`. Without it, a closed banner shows until hydration removes it. A Nuxt app that renders per user can skip `storageKey` and bind `v-model` to `useCookie`, so the server never renders a closed banner, at the price of HTML that can no longer be cached as one page.

  It is a named `region` landmark ("Announcement"), not `role="alert"`. An alert that arrives with the page is not announced, and one inserted later interrupts the screen reader. It stays in normal flow, not sticky: a sticky strip permanently covers content and can hide the focused element (WCAG 2.4.11). Put it first in the layout, above a sticky header, and show one at a time. Two new locale strings back it: `banner.label` and `banner.close`.

## 2.45.0

## 2.44.0

### Minor Changes

- 7cd484e: KunAvatar draws avatar frames. `KunUser` gains an optional `avatarDecoration: { src, animatedSrc? }`, and any avatar whose user carries one gets the frame around it — `KunUserChip` included, with no change at the call site. The asset is a square canvas 1.2× the avatar with the avatar circle centred, the geometry Discord's decorations and Steam's avatar frames use; the frame hangs 10% outside the avatar on every side as an absolutely positioned, `pointer-events: none`, `aria-hidden` overlay, so it moves no layout and never takes the avatar link's click.

  The new `decoration` prop decides when the animated asset plays: `hover` (the default) shows the still frame and plays the animation while the avatar is hovered or focused, `always` plays it continuously, `static` never animates, and `none` hides the frame. Frames are not drawn below the `md` size, where they are illegible, and `KunAvatarGroup` never draws them — its 4px gap cannot hold two frames overhanging by 10%. A reader who asked for reduced motion always gets the still image, through a native `<picture>` `<source media="(prefers-reduced-motion: reduce)">`, so it is right in the server-rendered HTML with no script.

  Nothing changes for a user without `avatarDecoration`: no extra element, no inline style.

## 2.43.0

### Minor Changes

- 070a75a: **`KunImages.loadingImage` reaches the Flutter icon package.** `KunLoading`'s mascot has always been inlined in `@kungal/ui-vue` as `KUN_LOADING_IMAGE`, but the Flutter generator emitted only `nullImage` and `avatarFallback`, so `kun_ui_icons` carried two of the three bitmaps KunUI draws. A Flutter port of `KunLoading` therefore could not exist: the component's default form _is_ that image, and taking it as a required parameter instead would be a different component from the web's. The generator now decodes it alongside the other two — the same 500×333 WebP the web renders, bundled as a package asset, so nothing needs declaring in a consumer's own `pubspec.yaml`. Nothing changes for a web consumer.

  **`KunProgress` no longer reports a range a screen reader cannot use.** `aria-valuenow` has always carried the _percentage_, while `aria-valuemax` carried `max` — so a bar of 60 out of 60 announced "100 out of 60" to assistive technology whenever `max` was not 100, and the ratio a screen reader computes from the two was wrong for every `max` but the default. The ceiling is `100` now, which is the range `aria-valuenow` was already being measured against, and it agrees with the percentage `showLabel` renders. `max` still scales the input; it simply is not the reported ceiling. No visual change, and no template change for a consumer.

## 2.42.3

## 2.42.2

## 2.42.1

## 2.42.0

## 2.41.0

### Patch Changes

- 7176e08: **KunAvatar no longer warns about an empty `avatarFallbackPool` when there is no user.** The dev warning exists because users with no avatar image all get the same fallback when the pool is empty. A `null` or `undefined` user always gets one shared image, whether or not a pool is configured, so the warning does not apply to it. It fired anyway. Because it fires only once per page, an app whose users all have avatars saw it while the user was still loading. It now fires only when it renders a user who has no `avatar`. That includes a user who arrives after the first render.

  **`id: 0` is now documented as "no profile".** KunAvatar and KunUserChip have always rendered a user whose `id` is 0 without a link. Apps rely on this for an unknown or deleted author, or for a signed-out viewer. The docs for `KunUser.id` and `isNavigation` now say so.

## 2.40.3

## 2.40.2

## 2.40.1

## 2.40.0

## 2.39.0

## 2.38.0

## 2.37.0

## 2.36.0

## 2.35.1

## 2.35.0

### Minor Changes

- 91109ca: Motion crosses to Flutter as physics, not keyframes (tier 2 of the Flutter roadmap).

  KunShatter's ballistic model — outward impulse, air-drag decay `1 − (1−t)^1.7`, gravity as a t² acceleration, per-shard scale/fade envelopes, the outward-propagating stagger — now lives in one shared manifest (`packages/ui-tokens/scripts/motion-physics.mjs`) instead of inline literals in the component. Two artifacts are generated from it:

  - `KUN_SHATTER_PHYSICS`, a new export of `@kungal/ui-core`, which `KunShatter` now consumes. Same numbers as before — the baked keyframes are unchanged, so nothing moves differently.
  - `KunShatterPhysics` in the `kun_ui_tokens` pub package, documenting the full model, so a Flutter shatter samples the identical trajectories instead of re-tuning the feel by eye.

  The four cubic-bezier easings and the duration scale already crossed in 2.33.0 as `KunEasing`/`KunDurations` — they need no sampling at all (`Cubic(a, b, c, d)` is the same parametrisation as CSS `cubic-bezier`). The ballistic motion is the part that cannot cross as a curve constant, and its parameters are one level above the samples: the web bakes WAAPI keyframes from them at runtime, Flutter can bake the same tables or run them through its own simulations. Sampled spline tables (`CatmullRomCurve.precompute`) were rejected — every curve in this model has a closed form, and a table would only approximate what the exponents state exactly.

  Nothing to do as a consumer of the npm packages.

## 2.34.0

### Minor Changes

- 13d9bb6: Icons now generate into two targets: the web bundle and a `kun_ui_icons` package on pub.dev

  The icon list in `@kungal/ui-core` has always been the single source for every
  icon KunUI renders — a fixed set, extracted from `@iconify-json` at build time
  and inlined, so the library never fetches an icon at runtime. It now emits two
  outputs from that one list instead of one:

  - **`icons-data.ts` — byte-identical.** Nothing about the web layer changes.
    Same icons, same data, same bundle size. The list itself moved out of
    `gen-icons.mjs` into `scripts/icons-manifest.mjs` so both generators read it;
    the generated file is unchanged.
  - **`kun_ui_icons` on pub.dev**, generated into `packages/ui-icons-flutter`.
    The NextMoe/KunGal apps are Flutter, and Flutter has no DOM to hand an SVG
    body to — its native icon delivery is a font plus `IconData`, which is what
    buys free `IconTheme` colour and size inheritance, `--tree-shake-icons`, and
    no runtime dependency. So the generator outlines the icons and packs them
    into a bundled TTF with one `const IconData` each:
    `Icon(KunIcons.circleCheck)`. Its version is locked to the npm version, like
    `kun_ui_tokens`: `2.33.0` here is `2.33.0` there.

  lucide draws with **strokes**, and a font glyph has no stroke — only filled
  contours. The generator therefore runs lucide's own font pipeline (rasterise at
  800px, trace back to an outline, pack at a 1000-unit em), which is the proof
  these exact shapes convert: it is how lucide's official icon font is built. The
  codepoints are allocated once into a committed manifest and never reassigned,
  so an app compiled against an older version keeps rendering the icon it
  compiled against.

  One icon does not cross. `svg-spinners:90-ring-with-bg` is an _animated_ SVG,
  and no static font format carries motion, so the Flutter package ships 30 of
  the 31 bundled icons; a Flutter app wants a real progress-indicator widget for
  that one anyway. Nothing about the spinner changes on the web.

  Nothing to do as a consumer of the npm packages: no component, no icon and no
  generated data changed.

## 2.33.0

## 2.32.1

## 2.32.0

## 2.31.1

## 2.31.0

## 2.30.0

## 2.29.0

### Minor Changes

- bd3836f: `rounded="full"` no longer paints 9999px onto a floating panel: the popups of KunSelect / KunAutocomplete / KunDatePicker fall back to `lg`

  > From this release the changelog is written in English.

  **The bug**

  All three are trigger + panel components, and all three applied the same `roundedClass` to both. On the trigger — a single-line control — `full` is a pill and is correct. On the panel it is not a pill at all: per CSS Backgrounds 3 §5.5 the browser scales every corner by `min(side / sum of that side's radii)`, so the radius actually painted is half the panel's short side.

  Measured in this repo's own docs (`/components/select`, the FilterBar example, Chromium):

  | panel                | declared | actually painted |
  | -------------------- | -------- | ---------------- |
  | 218×280 tag list     | 9999px   | 109px            |
  | 80×116 platform list | 9999px   | 40px             |

  The 80×116 one was a lozenge with its option labels pushed into the curve. KunDatePicker's 276×324 calendar was 138px.

  This was not a misconfiguration downstream. **KunUI's own example prescribed it** — `apps/docs/app/examples/select/FilterBar.vue` sets `rounded="full"` on three `KunSelect`s and overrides only `classNames.trigger`. Anyone copying the example shipped the blob.

  **The change**

  New `kunPanelRoundedClass(rounded)` in `@kungal/ui-core`: `full` resolves to `lg`, the other four buckets pass through unchanged. The three components' floating panels use it; their triggers are untouched.

  `lg` is not an invented number — `--radius-kun-lg: 16px` is documented in `tokens.css` as “containers / floating panels”. HeroUI is built the same way: the five buckets of its select `radius` variant write **only** `trigger`, and `popoverContent` takes its radius from Popover, whose default is `lg`.

  **What it costs you**

  - If you set `rounded="full"`: the popup becomes a 16px rounded rectangle; the trigger stays a pill. That is the only visible change.
  - If you already corrected it with `classNames.popup`: your class still wins (since 2.27.0 `cn()` understands `rounded-kun-*`). Nothing changes for you.
  - The other four buckets (`none` / `sm` / `md` / `lg`) behave exactly as before.
  - If you genuinely want a 9999px panel: `classNames.popup="rounded-kun-full"`. KunAutocomplete has no `classNames` at all — which is also why this had to be fixed in the library: its consumers had no local escape hatch.

  **KunPopover / KunModal / KunDrawer / KunTooltip are unchanged.** They have a single surface, so `rounded` names that surface directly and `full` there is an explicit request. What was fixed here is one prop quietly governing two surfaces, one of which has no correct value for `full`.

  **Docs site**

  The homepage's version badge and component counts are now read at build time from `packages/vue/package.json` and `KUN_COMPONENT_NAMES` instead of being typed by hand — they said “v0.2” and “53 个组件” while npm was at 2.28.0 with 70 components. Section 12 of `docs/INTEGRATION.md` (inlined verbatim into `llms-full.txt`) is now generated from the same list; hand-maintained, it had fallen 18 names behind, so AI tools reading `llms-full.txt` were told KunAccordion, KunCarousel, KunCommandPalette, KunSteps and friends did not exist.

## 2.28.0

## 2.27.0

### Minor Changes

- 5890c9c: `cn()` 现在认识 KunUI 自己的 Tailwind 比例尺；KunAutocomplete 补回输入法保护；组件文档补上事件与插槽

  上一次发布后做了一轮更严格的复测，这次修的是那轮报告里"根因还在"的部分。

  ## `cn()`：把 KunUI 的比例尺教给 tailwind-merge

  tailwind-merge 只认识 Tailwind 自己的比例尺，凡是它解析不了的类都原样放行、也就永远不会参与冲突消解。KunUI 在 `@theme` / `@utility` 里自己铸的那一批全都在这个盲区里：

  ```ts
  cn("rounded-kun-md", "rounded-full"); // 旧：两个类都留下，谁赢由 CSS 源码顺序决定
  ```

  实测后果是 `KunSelect` 的 `classNames.trigger: 'rounded-full'` 传下去圆角仍然是 12px —— 也就是说 `classNames` 文档里承诺的"你的类会赢"对 `rounded-kun-*`、`shadow-kun-*`、`z-kun-*`、`ease-kun-*`、`duration-kun-*` 和自定义 `animate-*` 全部不成立。

  `shadow-kun-*` 比其它几个更糟：shadow **颜色**组的校验器接受任意非任意值单词，所以 `shadow-kun-md` 被归类成了颜色，会和 `shadow-primary/20` 互相消解 —— 被丢掉的是那条真正的 box-shadow。

  现在 `cn()` 用 `extendTailwindMerge` 把这些名字注册进对应的组，`classNames` 的承诺第一次是真的。**名单由 `pnpm gen` 从 `tokens.css` 生成**（`packages/ui-core/src/twScales.generated.ts`），因为手写名单漂移之后不会报任何错，只会安静地退回旧行为。

  库内部只有两处的合并结果因此改变，两处都是"本来就想让后写的那个赢"：日期面板里区间中段的月/年格子（`rounded-kun-md` 之后跟 `rounded-none`）现在稳定是方角，禁用的 `KunTextarea`（`shadow-kun-sm` 之后跟 `shadow-none`）现在稳定没有阴影。

  ## KunAutocomplete：补回输入法保护

  和 KunSelect 上一轮修掉的是同一个洞。`:value` + 手写 `@input` 会绕过 Vue 自己的组合输入保护（`runtime-dom` 的 `vModelText` 在 `compositionstart` 上打 `e.target.composing` 标记，input 监听器以 `if (e.target.composing) return` 开头），于是用拼音打「你好」时列表会在每一次罗马字按键上重新过滤、塌成"无匹配项"，`@search` 也会在真正那一次之前先发五次半成品。现在按 `compositionstart` / `compositionend` 跟踪，一个词只发一次。

  ## KunDatePicker

  - **「今天」不再无视 `minDate` / `maxDate` / `isDateDisabled`。** 它绕开网格直接提交，所以当今天本身超出边界时它是唯一一条能一键选中被禁日期的路径。现在按钮直接禁用。
  - **面板的 ARIA 语义改成自洽的组合框 + 网格。** 原先面板是 `role="dialog" aria-modal="true"`，但焦点始终留在 trigger 上 —— 等于对辅助技术宣称"页面其余部分已惰性化"，而被聚焦的元素恰恰在那"其余部分"里。ARIA 1.2 只在 "`aria-controls` refers to an element that supports `aria-activedescendant`" 时允许组合框保留焦点并把 `aria-activedescendant` 指进弹出层，而 `dialog` 不在该属性的适用角色里、`grid` 在。所以：弹出层就是网格，trigger 改为 `aria-haspopup="grid"`、`aria-controls` 指向网格本身，面板不再是 dialog。与 KunSelect（`combobox` + `haspopup="listbox"` + `aria-controls` 指向 listbox）完全同构。
  - **三个网格补上 `role="row"`。** `role="grid"` 直接套 `role="gridcell"` 是不完整的，屏幕阅读器会把 42 个格子读成一个平铺列表，丢掉"第 3 列 / 共 7 列"。每一行现在是自己的 CSS grid，行永远是满的，所以视觉上和原来逐像素一致。星期表头按 react-aria `useCalendarGrid` 的做法标 `aria-hidden`（"the day names are already included in the label of each cell"，我们的格子标签是完整的 `toDateString()`）。

  如果你的测试用 `[role="dialog"]` 找日期面板、或者用 `[role="grid"] > [role="gridcell"]` 直接取格子，需要相应改一下。

  ## KunTab

  `setTabRef` 的开发期警告用的是 `import.meta.env.DEV`，Vite 在构建本包时会把它折叠成 `false`，警告根本不会进入发布产物。改为 `process.env.NODE_ENV !== 'production'`（由**消费方**的打包器替换），与 KunModal / KunDrawer 一致。

  ## 文档：属性表之外的一半 API

  - **78 个公开 prop 的说明此前写成了 `//` 注释**，而只有 JSDoc 会进入属性表和 `llms.txt`。全部转成 JSDoc；`KunImage` 的 `@nuxt/image` 那一组原先共用一条组标题注释，现在每个 prop 各有一条。
  - **事件和插槽第一次出现在生成的文档里。** `gen:meta` 现在一并导出 `events`（74 个）和 `slots`（65 个），组件页新增「事件」「插槽」两张表，`<route>.md` 同步。此前 `@search`、`@invalid`、`#option` 这些不打开 SFC 就无从发现。25 个语义不能从名字读出来的事件补了 JSDoc。
  - 新增生成器 `pnpm gen:scales`（`gen` 链里排在 `gen:tokens` 之后），CI 的"生成物是否最新"这一关会顺带盯住 `cn()` 的比例尺名单。

## 2.26.3

## 2.26.2

## 2.26.1

## 2.26.0

## 2.25.1

## 2.25.0

## 2.24.0

## 2.23.0

## 2.22.0

## 2.21.0

## 2.20.0

## 2.19.1

## 2.19.0

## 2.18.2

## 2.18.1

## 2.18.0

## 2.17.0

## 2.16.0

## 2.15.1

## 2.15.0

## 2.14.1

## 2.14.0

## 2.13.1

## 2.13.0

## 2.12.2

## 2.12.1

## 2.12.0

## 2.11.0

## 2.10.0

## 2.9.1

## 2.9.0

## 2.8.3

## 2.8.2

## 2.8.1

## 2.8.0

## 2.7.1

## 2.7.0

## 2.6.0

## 2.5.0

## 2.4.0

## 2.3.1

## 2.3.0

## 2.2.0

## 2.1.1

## 2.1.0

## 2.0.1

## 2.0.0

### Major Changes

- c0e69ae: Remove the `ghost` variant.

  `ghost` was visually indistinguishable from `bordered`: at rest both are just
  `border + bg-transparent + colored text`. `ghost` only added a faint `hover` fill,
  so the two looked identical until hovered. It has been dropped from `KunUIVariant`,
  which affects every variant consumer — `KunButton`, `KunChip`, `KunDropdown` and
  `KunInfo`.

  **Migration:** replace `variant="ghost"` with `variant="bordered"` (the outline
  look it overlapped). For a softer fill instead, `variant="flat"` or `variant="light"`.

  (This mirrors the earlier removal of the `faded` variant for the same reason; the
  remaining set — solid / bordered / light / flat / shadow — has no visual overlap.)

## 1.14.2

## 1.14.1

## 1.14.0

## 1.13.0

## 1.12.1

## 1.12.0

## 1.11.0

## 1.10.1

## 1.10.0

## 1.9.5

## 1.9.4

## 1.9.3

## 1.9.2

## 1.9.1

## 1.9.0

## 1.8.3

## 1.8.2

## 1.8.1

## 1.8.0

## 1.7.0

### Minor Changes

- 82de3b5: feat(tokens): regenerate the semantic palette in OKLCH with contrast-guaranteed on-colors

  The whole semantic color system is now **generated** (scripts/gen-tokens.mjs,
  OKLCH via culori) instead of hand-authored HSL. Each hue is defined once by its
  OKLCH hue + a vivid `solidL`; every shade is laid on a perceptual lightness ramp
  (so `-500` means the same perceived lightness for every color), and each color
  ships a paired **`--color-{c}-foreground`** on-color DERIVED by measured WCAG
  contrast. The generator asserts AA on every solid (fill, text) pair in BOTH light
  and dark and fails the build on any regression — illegible solids can't ship
  again. Adds the previously-missing `-950` shade.

  What changes visually: solids keep HeroUI-style vivid fills (bright amber warning,
  bright green success — no more muddy darkened `-600` fills), with white text on
  the medium hues (primary/danger/default) and a refined dark tint on the bright
  ones (secondary/success/warning/info). Solids are now mode-independent, so the
  per-variant `dark:bg-{c}-{n}` pins are gone. **This is a visual change** to every
  colored surface; the component API (color/variant names) is unchanged.

  `@kungal/ui-core`: `kunSolidClasses` / `kunSolidFgClasses` / `kunSolidBgClasses`
  and the Button solid/shadow rows now use `bg-{c} text-{c}-foreground`. CheckBox,
  DatePicker, Switch, Carousel drop their hardcoded white/black + dark pins.

## 1.6.3

## 1.6.2

### Patch Changes

- 05f2bee: chore: ship CHANGELOG.md in the published packages

  `CHANGELOG.md` is now included in each package's npm tarball (added to `files`),
  so downstream can read the per-version changes straight from the npm package
  page — not only from the GitHub repo. (Releases also now appear on GitHub
  Releases and the docs site's auto-generated /changelog page.)

## 1.6.1

## 1.6.0

## 1.5.0

### Minor Changes

- 13005ea: fix(vue): legible foreground on solid/filled color variants (esp. dark mode)

  Solid fills painted white text on `bg-{color}`, which has two problems verified
  by contrast measurement:

  1. The dark color scale is inverted, so a plain `bg-{color}` renders **pale** in
     dark mode — white text dropped to ~1.0–2.5:1 (the `solid` Info `info` callout
     was essentially invisible, white on near-white).
  2. The light hues (secondary / success / warning / info) are light in **both**
     modes, so white text fails WCAG everywhere (~2:1), not just in dark mode.

  New single source of truth in `@kungal/ui-core` — `kunSolidClasses`,
  `kunSolidBgClasses`, `kunSolidFgClasses` — pairs each fill with a `dark:bg-*`
  pin (stays saturated in dark mode) and a contrast-correct foreground: the dark
  hues (default / primary / danger) keep white, the light hues take dark text.
  Every solid foreground now clears WCAG AA in both modes (≈4.1–10.3:1).

  Applied to: Button / Chip (shared variant matrix), Info (`solid` / `shadow` — the
  reported bug; its title no longer overrides the box foreground), Badge, Progress
  (on-bar label), Tab (`solid` / `pills`), DatePicker (selected day), CheckBox
  (checked fill + check/dash mark), Switch (on-track).

  Visible change: `secondary` / `success` / `warning` / `info` solid components now
  use dark text instead of (illegible) white.

## 1.4.2

## 1.4.1

## 1.4.0

## 1.3.0

## 1.2.0

## 1.1.1

## 1.1.0

## 1.0.0

### Major Changes

- ac0bd4e: 1.0.0 — first stable release.

  The component set (57 Vue components) and the design-token system are stable and
  documented. Over the 0.14 → 0.22 line every cross-cutting surface was routed
  through a single source of truth: borders (`--color-kun-border` / `border-kun`),
  focus rings (`kunFocusRingClasses`), corner radius (`rounded-kun-*` /
  `--kun-radius-scale`), elevation (`--shadow-kun-*`), motion (`--kun-dur-*` +
  `duration-kun-*` + `ease-kun-*`), and sizing (`kunControlSize` /
  `kunSelectionSize` / `kunChipSize`).

  Also fixes a registration gap surfaced while completing the docs: `KunAutocomplete`,
  `KunNumberInput`, and `KunPinInput` (added in 0.14.0) were never added to the Nuxt
  layer's auto-import list, so Nuxt consumers hit "Failed to resolve component". They
  now auto-import like every other component (plain-Vue `app.use(KunUI)` already
  registered them). Their docs pages, prop tables, and `llms.txt` entries are added.

## 0.22.4

## 0.22.3

## 0.22.2

### Patch Changes

- 957cb52: fix(core): `shadow` button variant now actually casts its colored glow

  The `shadow` variant set a shadow _color_ (`shadow-{color}/40`) but never a shadow
  _size_, so `--tw-shadow` stayed empty and the button rendered with `box-shadow:
none` — it looked identical to `solid`. Added `shadow-lg` to every entry so the
  geometry exists and the tint applies: each shadow button now floats with a soft
  diffuse glow in its own color (the button's `overflow-hidden` doesn't clip an
  outset box-shadow, so it shows in both themes).

## 0.22.1

### Patch Changes

- be17775: fix(core): consistent dark-mode fill for solid / shadow buttons

  Filled (`solid` / `shadow`) buttons keep white text in both themes, but the dark
  color scale is inverted — `bg-{color}` (a `-500`/`-600` step) renders _light_ in
  dark mode, so the fills came out pale and at wildly different levels: `info`
  ≈ L88% (near-white, white text barely legible), `default` ≈ L65%, `secondary`
  ≈ L72%, and `success` disagreed between the two variants (solid pinned
  `dark:bg-success-300` ≈ L35% while shadow used the un-pinned `bg-success-600`
  ≈ L66%).

  Each color now pins a `dark:bg-{color}-{n}` so every solid/shadow button lands at
  a consistent ~L44–55% in dark mode (info/success/default → ~L44–46, the rest
  ~L47–55) — one saturated tier with legible white text. Light mode is unchanged.

## 0.22.0

## 0.21.0

### Minor Changes

- 3e841f0: feat: align form labels / error text and unify the chip-tag size scale

  The core size system was already consistent (form controls share
  `kunControlSizeClasses`, checkbox/radio share `kunSelectionSizeClasses`). The
  drift was in the peripheral bits:

  - **Form labels** now identical everywhere: `KunTextarea` and `KunDatePicker`
    labels gained the `text-default-700` tint, and `KunDatePicker` dropped its odd
    `mb-2` for the standard `mb-1`.
  - **Error messages** now identical: `KunTextarea` switched from `text-danger-600`
    (and a `<div>`) to the standard `text-danger` `<p>`; `KunDatePicker` and
    `KunRadioGroup` dropped `mt-2` for `mt-1`.
  - **Chip / tag size**: new `kunChipSizeClasses` in `@kungal/ui-core` is the single
    source for chip/tag pills. `KunChip` and the tags inside `KunTagInput` now share
    it (and the pill `rounded-full` shape), so a tag looks identical to a standalone
    `<KunChip>` of the same size instead of being a one-off smaller rounded-rect.

  Tab keeps its intentionally-compact tab scale; Switch/Slider keep their
  dimension-specific scales.

## 0.20.0

## 0.19.1

## 0.19.0

### Minor Changes

- d8e7e76: feat: KunTab `align` prop + one unified focus ring across every control

  **KunTab `align`** — new `align?: 'start' | 'center' | 'end'` (default `'center'`)
  controls how each tab's content sits inside its box. Mainly for vertical /
  full-width tabs, where the box is wider than its label.

  **Unified focus ring** — focus indication was a mess: `:focus` vs `:focus-within`
  vs `:focus-visible`, ring widths `1`/`2`/`4`, opacities `/25`/`/40`/`/50`/full,
  some controls dropped their border to fake a ring (a jarring jump), and Button /
  CheckBox had **no** focus ring at all. Everything now routes through one recipe:

  - New `kunFocusRingClasses` (direct controls) and `kunFocusRingWithinClasses`
    (composite wrappers) in `@kungal/ui-core`. One recipe: keyboard-only
    (`focus-visible`; text fields still show it on click), a flush **2px** ring in
    the control's semantic color at **/50**, no border-transparent jump.
  - Migrated Input, Textarea, Select, Autocomplete, NumberInput, DatePicker,
    PinInput, TagInput, Pagination, RadioGroup, **Button** (offset ring, added) and
    **CheckBox** (added) onto it. Composite widgets (NumberInput / TagInput) ring
    the wrapper via `focus-within` and the inner `<input>` has no ring of its own,
    so there's exactly one indicator.
  - Invalid controls turn the ring **danger** (same mechanism, swapped color).
  - **Deprecated:** `kunRingClasses` (mixed `:focus`/`:focus-within`, off-opacity).
    Use `kunFocusRingClasses` / `kunFocusRingWithinClasses`.

  No prop/API removals — purely additive plus a visual refinement of focus states.

## 0.18.1

## 0.18.0

## 0.17.2

## 0.17.1

## 0.17.0

## 0.16.0

## 0.15.0

## 0.14.0

### Minor Changes

- cd404aa: 表单 / 输入控件第一批升级:对标 HeroUI / Mantine / Ant Design / PrimeVue / Naive,补齐 API 完备性、一致性与高级控件。

  新增组件

  - **KunNumberInput** —— 数字步进输入:`min`/`max`/`step`/`precision` 钳制与四舍五入、−/+ 步进按钮(到边界自动禁用)、`ArrowUp/Down`·`PageUp/Down` 键盘、`role="spinbutton"` 无障碍、`name` 原生表单收集。
  - **KunPinInput** —— OTP / 验证码分段输入:`length`、`type`(numeric/text)、`mask`、自动前进/退格回退、粘贴自动分发、方向键、`complete` 事件、`autocomplete="one-time-code"`。
  - **KunAutocomplete** —— 组合框(combobox):文本输入 + 建议列表,客户端过滤或 `manualFilter` + `@search`(远程),`allowCustomValue`、`clearable`、键盘导航、`aria-autocomplete`。

  升级

  - **KunSelect** —— 补齐键盘可达性(P0):方向键 / Enter / Space / Esc / Home / End / 首字母 type-ahead + `aria-activedescendant`,禁用项自动跳过;新增 `searchable`(列表内过滤)、`multiple`(可移除 chips)、`clearable`、`description`、`name`(隐藏域)、选项 `disabled`。
  - **KunSlider** —— 默认 `min`/`max` 由 17–77 改为 **0–100**;修复 `reactive(props)` 拷贝导致改 prop 不更新的响应式缺陷;新增 `disabled`、`label`/`ariaLabel`、`error`/`description`、`color`、`marks`、值气泡 `showTooltip`、`showValue`、`formatValue`、`change` 事件。
  - **KunCheckBox** —— 新增 `indeterminate`(三态,用于全选)+ `error`/`description`。
  - **KunInput** —— 新增 `isClearable`、`revealPassword`(密码可见性切换)、`isInvalid` + `aria-invalid`/`aria-describedby`。
  - **KunSwitch** —— 新增 `error`/`description`。

  统一

  - 辅助文案统一为 `description`(对齐 HeroUI / React-Aria);`helperText`(Input/TagInput)与 `hint`(Textarea/FileInput/Upload)保留为 **已弃用别名**,仍可用,内部回退到 `description`。

  Breaking(0.x minor)

  - `KunSlider` 默认 `min`/`max` 改为 0–100(此前 17–77):依赖旧默认值的调用需显式传入。
  - `KunSelect` 的 v-model 类型放宽为 `T | T[] | null`(支持 `multiple` 与清除);单选用法不受运行时影响,仅 TS 类型变宽。

  新增打包图标(构建期内联,运行时零请求):`lucide:minus`、`eye`、`eye-off`、`search`。

## 0.13.0

## 0.12.0

## 0.11.0

## 0.10.0

## 0.9.0

## 0.8.0

### Minor Changes

- 7624924: Extend the unified size system to the non-text controls.

  The first sizing pass only covered text controls (button/input/select/…). This
  brings the selection + display controls onto the same coherent system, grounded
  in how HeroUI / PrimeVue / Naive UI / Mantine / Ant size them.

  - **New shared selection scale (`kunSelectionSizeClasses`, @kungal/ui-core)** —
    KunCheckBox and KunRadioGroup now use **identical** box sizes (every major
    library does this), so a checkbox and a radio of the same size match. Box px by
    size: 12 / 14 / 16 / 20 / 24 — ≈ 0.5× the text-control height and ≈ 1.2–1.4× the
    label font, so the box sits optically level with its label.
  - **KunCheckBox gains a `size` prop** (`xs`–`xl`, default `md`). It was hardcoded
    at 20px while its sibling KunRadioGroup scaled 12→24 — now they share one scale
    (md box is 16px). The check glyph and label scale with it.
  - **KunSwitch gains a `size` prop.** Track/thumb scale on clean steps (track
    28×16 → 64×32, thumb = track height − 4); `md` is the original switch size.
  - **KunSlider gains a `size` prop.** Track 4→12px, thumb 14→28px; `md` unchanged.
  - **KunChip** moved onto its proper compact sub-scale (≈ 0.7× the button height at
    the same keyword — a tag is text + tight padding, not a tap target); its `md`/
    `lg`/`xl` vertical padding is slightly tighter so chips no longer read as tall
    as buttons.

  Components sized by their content/padding rather than a height (KunTooltip,
  KunDropdown/KunContextMenu menus, KunPopover, KunInfo) intentionally keep no
  `size` prop — no surveyed library gives them one.

## 0.7.0

### Minor Changes

- 29a39a7: Unify form-control sizing on one shared scale, and fix the `lg`/`xl` button
  proportions.

  - **New `kunControlSizeClasses` (@kungal/ui-core)** — a single source of truth for
    the per-size font + padding of every text-like form control. Padding-driven,
    `md` (~38px) as the anchor, `px:py` a clean 2:1, horizontal padding growing
    faster than vertical so a bigger control gets wider, not flatter.
  - **KunButton `lg`/`xl` fixed** — `lg` was `px-6 py-2` (3:1) and `xl` was
    `px-8 py-2.5` (3.2:1, a wide flat bar). They're now `px-5 py-2.5` and
    `px-6 py-3` (both 2:1), so large buttons look proportional. `md` is unchanged.
  - **One scale across controls** — KunButton, KunInput, KunSelect, KunDatePicker,
    KunTextarea and KunTagInput all consume the shared scale, so a button, input,
    select and date-picker of the same size line up at the same height in a row
    (md = 38px).
  - **KunSelect / KunDatePicker / KunTextarea gain a `size` prop** (`xs`–`xl`,
    default `md`). Previously they had no size and were locked one notch tighter
    than buttons (`px-3` / `p-3`); their default horizontal padding is now `px-4`,
    matching KunButton/KunInput `md`.

  Pill/compact display components (KunChip, KunBadge, KunAvatar) are intentionally
  not part of this form-control scale and keep their compact sizing.

## 0.6.2

## 0.6.1

## 0.6.0

## 0.5.2

## 0.5.1

## 0.5.0

## 0.4.1

## 0.4.0

## 0.3.4

## 0.3.3

## 0.3.2

## 0.3.1

## 0.3.0

### Minor Changes

- 9b8cbae: Remove the `faded` variant.

  `faded` (tinted fill + border) was visually almost indistinguishable from
  `ghost`, so it's been dropped from `KunUIVariant`. This affects every variant
  consumer — `KunButton`, `KunChip`, `KunDropdown` and `KunInfo`.

  **Migration:** replace `variant="faded"` with `variant="flat"` (tinted fill, no
  border) or `variant="bordered"` (visible colored border); `ghost` stays for the
  outline look it overlapped with.

## 0.2.5

## 0.2.4

### Patch Changes

- 0ec98f9: Fix invisible outline variants (`bordered` / `faded` / `ghost`) and the
  off-center checkbox check.

  - **Variant table**: entries set `border-{color}` but never a border _width_ —
    which paints nothing in Tailwind v4, so `bordered` / `faded` / `ghost` showed
    no border on KunButton, KunChip and KunDropdown. Every variant now carries an
    explicit `border` width (transparent on `solid` / `light` / `flat` / `shadow`
    so box sizes stay uniform when switching variants), so the outline variants
    render again.
  - **KunCheckBox**: the checkmark was a full-size (1em) icon nudged down by its
    baseline offset, so it sat off-center and cramped the 20px box edge-to-edge.
    It's now an explicitly-sized 14px check centered with flexbox.

## 0.2.3

## 0.2.2

## 0.2.1

## 0.2.0

### Minor Changes

- 35358f2: Settle on the `@kungal/ui-*` package namespace; the four packages are versioned and released together.

## 0.1.1

### Patch Changes

- c532a02: Add npm `keywords` to every package for better discoverability on the npm registry.
