更新日志
KunUI 各版本变更(四个包 ui-tokens / ui-core / ui-vue / ui-nuxt 锁步同版本)。每条都源自该次发布的 changeset,**随发布自动生成**,无需手写。
从旧版本升级?
feat(vue): KunTab auto-handles horizontal overflow (scroll + edge fade + chevrons)
A horizontal tab strip that outgrows its container now scrolls inside the container instead of widening the page — automatically, with no
scrollableflag and no manual overflow check. When the tabs overflow:- the overflowing edge fades to transparent via a CSS mask — background-independent, so it reads clearly on any surface (unlike a colored scroll shadow that blends in);
- a chevron button floats on each scrollable side (opt out with
:scroll-buttons="false"to keep just the fade); - the active tab auto-scrolls into view, so ← / → keyboard nav always keeps the selection visible.
New prop:
scrollButtons?: boolean(defaulttrue). The existingscrollableprop now only governs vertical tab columns; horizontal overflow is handled unconditionally.
fix(vue): KunTab vertical orientation now defaults to left-aligned content
The
aligndefault is now orientation-aware. A vertical tab list reads as a nav column, where left-aligned labels are the convention, so vertical tabs now default toalign="start". Horizontal tabs keep the classic centered look. An explicitalignprop still overrides either orientation — passalign="center"to restore the previous centered vertical tabs.
feat(vue): KunTab
#tabslot for custom tab content (badges, dots)KunTab is now generic over the item shape and exposes a
#tabscoped slot ({ item, index, active }) so you can render custom per-tab content — e.g. compose aKunBadgefor an unread count or an "unsaved" dot — instead of being limited to icon + label. Extra fields on the item (acount, adirtyflag, …) are typed inside the slot. The sliding indicator measures the button, so badge-widened tabs are tracked automatically. Defaults to icon + label (backward-compatible).
feat(vue): KunPagination animates the active page (sliding pill + "highlight leads")
The active page is now a single primary pill that slides between numbers (like KunTab's indicator) with a small elastic pop, instead of the highlight jumping. For mid-range pages — where the active number stays centered in the ellipsis window so the pill can't slide — the highlight leads: it first covers the adjacent number, then the number row scrolls (FLIP) to recenter and the pill rides back with it. Honors
prefers-reduced-motion; falls back to a solid pill before hydration. No API change.
feat(vue): add KunCommandPalette (⌘K command palette / spotlight)
The generic ⌘K palette SHELL — trigger + global shortcut, teleported dialog, autofocus, body scroll-lock, keyboard nav (↑↓ / Home / End / Enter / Esc), grouped results, safe match highlighting, and full a11y (dialog + combobox + listbox + aria-activedescendant) — with NO search logic baked in. You compute
items(flat or grouped) from thequeryit exposes viav-model:query(your own scoring / index / async fetch) and it renders + navigates them; selecting emits@select. Generic over the item shape, with#trigger/#item/#empty/#no-result/#footerslots, aloadingstate, and a configurableshortcut. The docs site's ⌘K search is now a thin consumer of it.
fix(vue): KunMessage no longer leaks toasts across SSR requests
The toast store is module-scope, so on the server it's a single array shared by every SSR request and never cleared there (the dismiss timer is client-only). A server-side
useKunMessage()— e.g. from a data-fetch error handler that runs during SSR — therefore piled up (deduped into a growingcount), baked into every page's SSR HTML, and vanished on hydration (empty client store → hydration mismatch). Two guards, both making toasts the client-only ephemeral UI they are:useKunMessage()is a no-op on the server (the store is never mutated there), andKunMessageProviderrenders nothing until mounted.
feat(vue): KunPopover gains a
fullWidthpropThe trigger was wrapped in two hardcoded
inline-blockdivs, so a consumer could never make the anchor span its container — external classes only reached the outer wrapper, not the innertriggerRef.<KunPopover full-width>now switches both wrappers toblock w-full, so a full-width trigger (e.g. afullWidthKunButton or a split button) fills the width. Defaultfalse(inline, content-width) — unchanged.
fix(vue): KunDropdown / KunContextMenu item labels align left, not center
Menu items are native
<button>s, which default totext-align: center; theflex-1label span inherited that, so short labels sat centered. Both item rows now carrytext-leftso the label starts at the left edge (icon → label), the expected menu-item layout.
fix(vue): KunMessage no longer jumps wider for a frame when dismissed
The leaving toast went
position: absolute; width: 100%, but the%resolved against the outerfixedcontainer's padding box — 2rem wider than the in-flow content width — so the toast visibly widened and spilled out the right edge for a frame before fading. TheTransitionGroupwrapper is now the containing block (position: relative), sowidth: 100%matches the in-flow width exactly.fix(vue): KunMessage toasts use semantic-colored border + count badge
Each toast's outline is now its own semantic colour (
ring-{color}/50) instead of a uniform neutral grey ring, and the de-dup count badge uses a matchingbg-{color}/10tint instead of the neutralbg-black/10. Each type now reads as one cohesive coloured surface in both light and dark.
feat(vue): KunAutocomplete & KunSelect support custom option rendering via
#optionBoth components are now generic over the option shape, so you can pass options with extra fields (avatar, description, …) and read them — typed — in a new
#optionscoped slot:<template #option="{ option, index, active, selected }">. Render a leading image, two-line text, badges, anything. Without the slot the plain label renders exactly as before (fully backward-compatible). Select keeps its check indicator outside the slot, and the option row now groups rich content at the left with the indicator at the right.
feat(vue): KunAutocomplete gains
loading+debouncefor async data sources:loadingshows a spinner in the dropdown (reusingKunLoading) instead ofnoResultTextwhile a remote@searchrequest is in flight — drive it from your fetch (true on request start, false when the options land).:debounce(ms) delays the@searchemit so you fetch once the user pauses, not per keystroke (the input text still updates instantly). The two mesh: while the debounce is armed the spinner already shows, so the gap before the request never flashes "no matches" — it's continuous from keystroke to results.:loadingTextsets the spinner caption (default '加载中…'). Fully backward-compatible:debouncedefaults to 0 (emit every keystroke, unchanged).fix(vue): KunAutocomplete no longer reopens the panel after picking an option
Clicking an option blurred the input, and the post-select refocus then re-triggered the
@focus-to-open — so the panel visibly closed and sprang back open. Options now@mousedown.prevent(keeping focus on the field, so no reopen), and the input opens on@clicktoo so clicking the already-focused field can still reopen the list. Keyboard selection was unaffected either way.
feat(vue): KunTabPanel gains a
loadingstate (dim + inert + aria-busy)<KunTabPanel :loading>marks a panel busy while async / lazy data resolves. It dims the panel to0.5opacity, makes itinert(no pointer or keyboard interaction) and setsaria-busyfor screen readers. The dim uses a delayed fade (transition: opacity 0.2s 0.2s linear— the ReactuseDeferredValuetrick): a load that resolves quickly clearsloadingbefore the dim ever paints, so fast tab switches never flicker; only a genuinely slow load visibly dims. It snaps back to full opacity the instant content is ready, and honoursprefers-reduced-motion.This is the stale-while-revalidate mechanism only — it dims content that is already there. For a true first load (nothing to dim), render a skeleton (e.g.
KunSkeleton) in the slot and leaveloadingoff, so the skeleton shows at full opacity; fliploadingon only when revalidating existing content. See the new "懒加载 / 加载中" docs example.
fix(vue): KunReaction honors
activeskin in action mode (menu-button reactions)The filled/coloured skin now follows the
activemodel in BOTH modes, not justtogglemode. This lets an action-mode (toggle="false") reaction be a controlled "menu button": wrap it as aKunPopovertrigger, bind:model-valueto your own state, and the click opens the picker instead of self-toggling while the skin still reflects your state. This is what a 收藏 button needs when it sits next to a 点赞 reaction — both stay peer pills (identical skin), but 收藏's click opens a 收藏夹 picker and its filled state = "in ≥1 list" (Bilibili / YouTube pattern), no split button. Fully backward-compatible: existing action-mode buttons (share / more …) pass noactive, so they stay neutral exactly as before.
feat(vue): add KunButtonGroup (segmented actions + split buttons)
KunButtonGroupjoins a row/column ofKunButtons into one attached unit — collapsing the touching inner corners and overlapping the 1px borders into a single seam. It is the building block for a GitHub-style split button: a primaryKunButtonnext to a chevronKunButtonthat triggers aKunPopoverholding a rich menu (e.g. aKunCheckBoxGroupof lists + a "create list" footer). The seam CSS reaches a button nested inside aKunPopovertrigger wrapper, and — because the popover panel teleports to<body>— never touches the menu's own buttons. Supportsorientation="horizontal" | "vertical".
fix(vue): KunModal panel is opaque by default (drop the hardcoded 85% alpha)
The modal panel hardcoded
bg-content1/85, forcing an 85%-opaque (see-through) surface that ignored--kun-surface-opacityand stacked on top of it — so on a site that already lowered that token (a background-image page) the panel went even more translucent than every other surface. It now uses plainbg-content1like Card / Drawer / Dropdown / Tooltip / Select, so it is fully opaque by default and follows--kun-surface-opacity(set it < 1 with--kun-backdrop-filterto opt every surface into frosted glass at once). The backdrop scrim is unchanged.
fix(vue): KunTab active-tab text color now tracks the sliding indicator
The per-tab text color transitioned over Tailwind's default 150ms while the sliding indicator slid over 250ms (
--kun-dur-base), so the newly-active tab's text reached its final color ~100ms before the pill arrived under it. Withsolid/pillsthat meant the text went white over the still-uncovered light background and read as "invisible until the animation finished". The tab text transition is now pinned to the indicator's duration and easing (duration-kun-base ease-kun-standard), so color and position land in lockstep.
fix(vue): KunTab
borderedandpillsnow slide the active indicatorThe sliding indicator (the element that animates between tabs via
transform) was only rendered forunderlined/solid/light—borderedandpillsfell through toindicatorClassesreturningnull, so their active state only did an in-place color fade and looked like it had no switch animation. Both variants now get the same measured, sliding indicator (a rounded-full solid pill forpills, a colored outline forbordered), with the per-tab fill/border gated to the pre-hydration fallback exactly likesolid/light— no hydration flash, no layout shift. RespectsdisableAnimation.
feat(vue): add KunCheckBoxGroup + pill/icon selectors on KunRadioGroup
Adds the multi-select form field the library was missing, and rounds out the single-select one, so "pick a category / pick sections / pick types" selectors (previously hand-rolled in every downstream app) come from KunUI:
- New
KunCheckBoxGroup— WAI-ARIA checkbox-group semantics (value is a realT[]the form submits, unlike a toolbar-style toggle group). Variantsclassic | pill | card, amaxcap that blocks extra picks and emitsinvalid: 'max-reached', and per-optionicon/description. KunRadioGroupgains apillvariant (single-select "choice chips"), an optionaliconper option, andhideIndicatorfor thecardvariant (drop the radio dot and signal selection with the tinted border alone — the icon-card look).
Both share the selection size scale, color matrix, and focus ring with the rest of the library. Picking between them follows the recognized rule: single-select form field → RadioGroup, multi-select form field → CheckBoxGroup; a ToggleGroup stays for non-form UI toggles.
- New
fix(vue): ThumbHash blur-up now reliably shows before fast/cached images load
Both KunImage (covers) and KunContent (body images) decoded the ThumbHash through a lazy
import('thumbhash'). A fast or cached CDN image could finish loading during that import — after which a placeholder is pointless and was skipped — so the blur never appeared (you'd see the reserved box but no blur). The decode is now a synchronous (static) import, painted in the same tick as mount/scan, so it always wins the race against the image load. The decoder stays externalized + tree-shaken; it's ~2KB.
feat(vue): KunContent — ThumbHash blur-up for body images
Body images in KunContent are raw
<img>from v-html (backend-rendered markdown), not components, so they couldn't get the cover-image blur-up. Now any<img data-thumbhash="…">in the prose automatically shows a decoded, blurred placeholder until it loads.The decoded ThumbHash is painted as the image's OWN
background(visible behind the not-yet-loaded content, cleared once it paints over) — zero DOM restructuring, so it coexists with the existing lightbox and spoiler passes and never disturbs prose layout. The blur shows when the<img>reserves space (width/height attributes), which the same backend metadata supplies — together they also remove the load-time layout shift (CLS).Also exported as
useContentBlurUp(containerRef)for apps building their own prose renderer. Client-only; the ~2KB decoder is lazy-imported.
feat(vue): KunImage — ThumbHash blur-up placeholder
KunImage gains a
thumbhashprop: pass the base64 ThumbHash a backend ships alongside image metadata, and KunImage shows a decoded, blurred "blur-up" placeholder until the image loads, then cross-fades to it — much closer to the final frame than a plain skeleton, with no extra network request.- Decoded on the client (canvas) to a tiny data-URL image, upscaled by
bg-cover; SSR-safe — falls back to the pulse skeleton until decoded, or if the hash is invalid. - The ~2KB
thumbhashdecoder is loaded via a dynamic import, so it only ships for images that actually use the prop — zero cost otherwise.
- Decoded on the client (canvas) to a tiny data-URL image, upscaled by
feat(vue): KunScrollShadow — wheel-to-scroll, drag-to-scroll, and a scrollbar toggle
A horizontal
KunScrollShadowcouldn't be scrolled by mouse users (the wheel scrolls the page, not the strip). Three opt-in props fix that, all reusable:wheel— whenaxis="horizontal", a vertical mouse wheel scrolls the content sideways (horizontal trackpad swipes too).truereleases at the edges so the page keeps scrolling (no scroll-trap);'contain'keeps the wheel on the strip at the edges so the page doesn't move — only while the strip is actually scrollable, so it can never freeze the page.draggable— click-and-drag with a mouse/pen to scroll, like grabbing a strip. A drag past a small threshold suppresses the trailing click so cards inside still work on a normal click; touch is left to native scrolling.scrollbar—'hide'(default, unchanged),'thin'for a slim, theme-coloured CSS scrollbar (a dependency-free alternative to an overlay-scrollbar library), or'auto'for the platform default.
Performance: the wheel/drag handlers do O(1) work per event and read no layout in the hot path — scroll bounds come from the ResizeObserver-backed sizes, and the non-passive wheel listener is bound only when
wheelis on. No API breakage; defaults preserve current behaviour.
feat(vue): add KunShatter — break any content into glass shards that fly apart
A new animation component that shatters its slotted content (an image, a card, anything) into Voronoi glass shards which burst outward from an impact point, arc under gravity, spin, and fade.
Performance-first and dependency-free:
- The fly-apart is compositor-only — every shard animates only
transform+opacity(the two properties that run on the compositor thread, never re-running layout or paint), so it holds 60fps regardless of piece count. Verified: 0 dropped frames even at the 160-piece cap. clip-pathcarves each shard's glass edge but is set once and never animated (animating clip-path is not compositor-accelerated yet).- Each shard is sized to its own bounding box with
overflow:hidden+ paint containment, so N shards tile to ≈1× the element's area instead of N× full-size GPU layers — the one-time build stays a few milliseconds even at the cap. - Voronoi shard geometry is computed in-component (rectangle clipped against seed-point perpendicular bisectors); no runtime dependency.
Usage: wrap content in
<KunShatter>and trigger viatrigger="click",v-model:shattered, or the exposedshatter()/restore()methods. Tunable withpieces,origin,spread,gravity,rotation,fade,duration,easing,seed,autoRestore, andkeepSpace. Honoursprefers-reduced-motion(instant hide, no shards) and is SSR-safe.- The fly-apart is compositor-only — every shard animates only
fix(vue): KunCarousel — structurally eliminate the "runaway auto-advance" flicker
The seamless loop drives itself by writing
scrollLeft; some browsers then nudgescrollLeftagain after the reorder/reflow, which the re-home logic read back as a user scroll → re-home → nudge → a per-frame feedback loop (slides stacking and flickering, Chrome/Edge). 2.0.1'soverflow-anchor: noneonly closed one drift source (Chromium scroll-anchoring); other sources (snap re-alignment after theorderreflow) could still drive it.Three layers of defense so it can't recur regardless of the browser:
overflow-anchor: noneis now applied as an inline style instead of a Tailwind utility — a correctness fix must not depend on a headless consumer's Tailwind regenerating an arbitrary class; an inline style always wins and is never purged.- A re-entrancy lock: the carousel's own programmatic
scrollLeftwrites can no longer trigger a re-home, which breaks the whole class of write→event→re-home loops — not just scroll-anchoring. - A circuit breaker plus an autoplay self-heal: if reorders ever spike it drops to a plain non-looping slider, and autoplay re-homes before advancing so a stray drift can never leave the carousel parked at a physical edge.
No API change.
feat(vue): overlays avoid viewport collisions by default + cap size on small screens
Aligns the floating overlays with Floating UI / Radix defaults so they never overflow the screen at an edge or on a short viewport:
- KunPopover now flips, shifts AND caps its size by default (
autoPositiondefaults totrue— it wasfalse, so a popover near an edge used to overflow). Setauto-position="false"to honourpositionverbatim. (Panels withshow-arrowskip the size-cap so the caret isn't clipped.) - KunDropdown and KunDatePicker now cap their height to the available space and scroll, instead of overflowing off-screen on a short viewport.
- New
maxSizeoption on the internaluseKunFloating(size() middleware: max-height/width + scroll) — one implementation reused across overlays. Select / Autocomplete already did this.
- KunPopover now flips, shifts AND caps its size by default (
feat(vue): KunPopover
opaqueprop — keep a menu solid on a frosted siteSites with a background image often lower
--kun-surface-opacityto frost every surface — which also makes popover/hover-menu panels translucent and hard to read.opaqueforces a solidcontent1background (from its raw channels, ignoring the surface-opacity alpha; still light/dark adaptive). Note this is the only reliable way: setting--kun-surface-opacity:1on the panel does NOT work, because the themed--color-content1is resolved at:root, not on the element.
fix(vue): KunPopover restores focus to the trigger on every close
Hardens focus handling found while stress-testing hover menus: if focus was inside the panel when it closed via a path that doesn't restore it itself — e.g. a hover
groupsibling stealing the open menu — focus is now pulled back to the trigger instead of being orphaned on the detached panel node. Covers all close paths (group steal, click-outside, programmatic).
feat(vue): KunPopover
trigger="hover"+useKunPointerMenu— navigation hover menus done rightAdds first-class hover menus without the usual traps, via a reusable composable
useKunPointerMenu(also exported):- Coordinate safe-triangle — on leaving the trigger you can travel to the
panel without it closing. Computed from
clientX/Y+getBoundingClientRect(), so it works even though panels areTeleported to<body>(DOM-containment safe-polygons break across portals; coordinates don't). openDelay/closeDelayand a sharedgroupso a row of menus switches instantly between siblings and keeps only one open (menu-bar feel).- No focus steal on hover (unlike the click open), and touch falls back to
click (
pointerTypegate) so the first tap doesn't follow a link — the classic a11y trap. Click / keyboard / Esc / click-outside all still work.
KunPopovergainstrigger('click' default | 'hover'),openDelay,closeDelay,group.KunDropdown(role=menu) stays click-only by design.- Coordinate safe-triangle — on leaving the trigger you can travel to the
panel without it closing. Computed from
fix(vue): KunCarousel runaway auto-advance / "wild flicker" on Chromium
The seamless loop is a scroll-jacked container — it programmatically reorders slides (CSS
order) and resetsscrollLeftto re-home. Chromium's default scroll anchoring reacts to that reorder/reflow by nudgingscrollLeftto keep an anchor element in view; the re-home logic then misreads the nudge as "the user moved to the next slide", advances, reorders again, and loops — so the carousel races through slides far faster than the autoplay interval (looks like everything flickering/stacking; reported on Chrome + Edge, desktop). The track now setsoverflow-anchor: none, handing scroll control entirely to the component. No API or behaviour change otherwise.
Remove the
ghostvariant.ghostwas visually indistinguishable frombordered: at rest both are justborder + bg-transparent + colored text.ghostonly added a fainthoverfill, so the two looked identical until hovered. It has been dropped fromKunUIVariant, which affects every variant consumer —KunButton,KunChip,KunDropdownandKunInfo.Migration: replace
variant="ghost"withvariant="bordered"(the outline look it overlapped). For a softer fill instead,variant="flat"orvariant="light".(This mirrors the earlier removal of the
fadedvariant for the same reason; the remaining set — solid / bordered / light / flat / shadow — has no visual overlap.)
随依赖更新。
fix(vue): DatePicker / Autocomplete focus-on-open no longer risks scrolling the page
Hardens the two remaining popup components that still used a bare
.focus(): DatePicker (focus the root on open) and Autocomplete (refocus the input after select / clear) now pass{ preventScroll: true }. The menu components already did this; this brings the last two in line so a portaled-panel open can never jump the page to the top. (The publicAutocomplete.focus()method keeps the default so the caller controls scroll intent.)
feat(vue): KunReaction
toggle(action mode) + default-slot visible labelTwo additive, backward-compatible hooks so a whole actions row can be one component, and so the text is part of the click target:
toggleprop (defaulttrue).false= a one-shot ACTION (share / 更多 …) in the same compact skin: no pressed state, no burst, just a tactile pop — handle it with a native@click. A reactions row no longer needs to mix in a heavier icon button.- Default slot = a visible label rendered INSIDE the button, so clicking the
TEXT (not just the icon) toggles too — the clean fix for "点 收藏游戏 文字也该收藏".
It inherits the active colour; when present it becomes the accessible name (the
labelprop is the aria fallback only when there's no visible label). Omit it to keep the compact icon-(+count) reaction.
feat: single source of truth for component registration +
KunUIResolverKills the class of bug where a newly-added component is registered for plain Vue but forgotten elsewhere (the recent
Failed to resolve component: KunReaction).One source —
KUN_COMPONENT_NAMES(exported from@kungal/ui-vue). The plain-Vue plugin types its registry asRecord<KunComponentName, …>, so a missing/extra entry is a compile error, not a silent runtime failure. The Nuxt layer's auto-import list and the docs meta now derive from this list instead of hand-maintaining their own copies — they can no longer drift.KunUIResolver(new, from@kungal/ui-vue) forunplugin-vue-components, matching Element Plus / PrimeVue: Vite apps get on-demand, tree-shaken auto-import of every KunUI component with zero registration and zero list — new components work automatically.import Components from "unplugin-vue-components/vite"; import { KunUIResolver } from "@kungal/ui-vue"; // plugins: [Components({ resolvers: [KunUIResolver()] })]
No change to existing usage (
app.use(KunUI), the Nuxt layer auto-import).
随依赖更新。
feat(vue): KunReaction —
#iconslot + arbitrarycolorTwo additive, backward-compatible hooks for fully custom reactions (e.g. a "推"):
#iconslot (scoped{ active }) replaces the whole glyph — an emoji, image or custom SVG, and it can differ by active state. Slot content still gets the pop + burst animations.colornow also accepts any CSS colour string (e.g. a brand#ff6a00), not just a palette key. The whole effect runs throughcurrentColor, so the icon fill, the pop and the burst (ring + sparks) all follow it with no extra wiring.
Existing
icon/ palette-colorusage is unchanged.
feat(vue): KunReaction — a compact like/reaction control with count
A purpose-built reaction control so a like + count doesn't bloat into a wide padded text button. It's a tight pill (icon + optional count), a proper toggle (
aria-pressed, accessible name includes the count), and it animates on click — all pure CSS/Vue, no external library:- icon fills + colours when active;
- a bouncy pop;
- a one-shot burst (expanding ring + radiating sparks) when liking;
- the count rolls in the direction it changed.
v-modelis the active state;v-model:countthe count (auto ±1 on click, the parent can override for server sync). Props:icon(default a heart),color(default danger),size,disabled,disableAnimation,label. All animation is off underprefers-reduced-motion.
fix(vue): vertical underlined Tab indicator jumping on hydration (SSR)
The pre-hydration fallback bar (drawn before the JS-measured indicator mounts) was hardcoded to the BOTTOM edge, so a vertical
underlinedtab showed its indicator under the active tab on the server and then jumped to the LEFT once the measured indicator took over. The fallback now follows orientation — a LEFT inset bar for vertical, BOTTOM for horizontal — so the SSR axis matches the final one and there's no jump.
feat(vue): KunCarousel seamless infinite loop (
loop, default on)Autoplay used to smooth-scroll all the way back to the first slide at the end — a jarring reverse sweep. KunCarousel now loops seamlessly by repositioning slides (a CSS
orderring), with NO cloned DOM: the slide physically to the right of the last is always the first, and after each scroll settles the position is re-homed in the same frame (only off-screen slides shuffle, so the reset is invisible). Autoplay glides forward past the end into the start; the arrows wrap both ways too.- New
loopprop, defaulttrue(auto-disabled when there are too few slides to loop without glitches; passloop="false"for the old bounded behaviour). - Reposition, not cloning → no duplicate nodes for screen readers to read twice.
- Keeps the native scroll-snap base (touch swipe + momentum + SSR).
Note: default-on, so existing carousels now loop — autoplay no longer snaps back, and the arrows wrap around instead of disabling at the edges.
- New
fix(vue): DatePicker trigger — gap + truncation between text and calendar icon
The trigger only had
justify-between(no gap), so in a narrow field the placeholder/value text butted right against the calendar icon with no spacing (and looked vertically off). Adopted the Select trigger's pattern:gap-2on the button,min-w-0 flex-1 truncateon the text, andshrink-0on the icon group — so there's always an 8px gap, the text truncates gracefully, and the icon stays put. (Audited Select, Autocomplete and the input family — they already do this; DatePicker was the only one missing it.)
fix: DatePicker month/year nav closing on mobile; Enter key hijacked to "Next"
- DatePicker: the calendar panel is teleported to
<body>, so its month/year nav buttons were treated as outside-clicks and closed the picker (felt on mobile, where you must tap the nav). Added the samedropdownRef.containsguard that Select/Autocomplete already use; outside clicks still close it. - Mobile "Next" key: on a page with several fields the virtual keyboard shows a
"Next" action that jumps to the next field instead of firing Enter — breaking
inputs whose Enter does an in-component action. Declared
enterkeyhinton those: TagInput (enter, add tag), Autocomplete & searchable Select (done, pick the active option), Pagination jump field (go). Plain Input/Textarea/NumberInput/ PinInput are unchanged (field-to-field "Next" is correct there).
- DatePicker: the calendar panel is teleported to
perf: stop shipping backdrop-filter on every surface (mobile scroll jank)
KunCard shipped
backdrop-filter: blur(var(--kun-background-blur))on EVERY card — and the default blur was0pxover an opaque surface, so it did nothing visually while still promoting each card to a compositing layer and running the backdrop pipeline.backdrop-filter: blur()is the #1 cause of janky scrolling on mobile (a 120Hz phone can drop to ~30–60Hz). With many cards per page the layers piled up.- KunCard / KunModal now emit
backdrop-filteronly via the new opt-inkun-backdroputility, which isnoneby default (free — no layer, no blur pass). - New token
--kun-backdrop-filter(defaultnone) replaces--kun-background-blur. A glass site opts in for every raised surface at once::root { --kun-surface-opacity: 0.7; --kun-backdrop-filter: blur(12px); }
BREAKING (glass only): if you set
--kun-background-blur: 12px, switch to--kun-backdrop-filter: blur(12px). Sites that never enabled glass are unaffected (and get smoother scrolling for free).- KunCard / KunModal now emit
fix(tokens,vue): softer neutral hairline + bordered cards by default
- KunCard shows a faint hairline border by default again (it was borderless during the filled-surface work) — with the lighter page and softer shadows, a hairline delineates the card better than shadow alone.
- The shared neutral border token (
--color-kun-border/ theborder-kunutility) drops fromdefault-200todefault-100— a lighter hairline that delineates a surface without framing it. Every consumer softens at once: inputs, textarea, select & other controls, accordion, tabs, dividers, drawer rules, etc. Error borders (danger) and focus rings are unaffected.
fix(vue,tokens): bordered inputs, softer shadows, lighter page background
- Form controls (Input, Textarea, Select, NumberInput, Autocomplete, DatePicker, TagInput flat, PinInput, Pagination jump field, Select's inline search) get a card-like neutral border back on top of the filled surface — the borderless fill was too hard to spot on a card. Error state recolours the border to danger instead of a persistent ring. (= the shadcn "border + fill + subtle shadow" input.)
- Elevation scale softened ~30% across all three tiers (sm/md/lg) — lighter, tighter shadows on cards, inputs, dropdowns, modals.
- Light page background nudged brighter (#f2f2f5 → #f4f4f7). Dark unchanged.
feat(vue): KunCard
paddingprop + roomier default; bump KunInfo paddingKunCard's inner padding was
p-3(12px) — tighter than the modern norm (shadcn / Ant use 24px, MUI 16px) and tighter than KunModal's own 24px, which made card-heavy UIs feel cramped.- New
paddingprop on KunCard:none|sm(12px) |md(20px) |lg(24px), defaultlg. Inner sectiongapalso grows 12px → 16px. Passsmfor the old compact density,nonefor a full-bleed card (e.g. just a cover image). - KunInfo padding 12px → 16px to match.
Visual change: cards/info are roomier by default. Set
padding="sm"to keep the previous density.- New
随依赖更新。
fix(tokens,vue): give surfaces breathing room + refine KunCard hover
- The light page background is a touch deeper (
#f5f5f7→#eeeef1) so white cards/surfaces pop more (≈17 vs ≈10 units) and there's room for interaction states. Dark mode is unchanged (it already had ample headroom). - KunCard hover feedback now applies only to interactive cards (
href/clickable) or an explicitisHoverable; a plain static card no longer reacts. - Hover is a faint
foregroundstate layer (≈3%, via::after) — darkens slightly in light, lightens in dark — and stays clearly brighter than the page (no surface-colour swap, no shadow change). - KunNumberInput stepper buttons use
hover:bg-foreground/8(a normal control hover) instead of the absolutebg-default-100.
- The light page background is a touch deeper (
随依赖更新。
feat(tokens,vue): surface-elevation system — cards & inputs pop by fill+shadow, not borders
Move from a border-defined look to an elevation scale. The page background is now a soft neutral (light
#f5f5f7, dark near-black#0a0a0a) instead of pure white/black, so raised surfaces read as raised:- Card is a raised surface —
bg-content1(#fff/#18181b) +shadow-kun-sm; border is now OFF by default (borderedis opt-in). It no longer shares the page background. - Inputs are borderless and share the card surface: Input, Textarea, Select,
NumberInput, TagInput, PinInput, Autocomplete, DatePicker trigger and the
Pagination field use
bg-content1+shadow-kun-sm(same fill as a card, lifted by a small shadow). The error state is a danger ring, not a border. - Floating panels lose their border and rely on shadow + the
content1surface: Dropdown, Select/Autocomplete lists, ContextMenu, Popover, Tooltip, DatePicker calendar, Modal, Drawer. - Placeholder now uses a theme-adaptive
::placeholdercolour (the browser default grey didn't follow light/dark).
Visual change only; component APIs are unchanged except
KunCard'sbordereddefault (true→false) andKunTagInput'svariantdefault (bordered→flat).- Card is a raised surface —
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-500means the same perceived lightness for every color), and each color ships a paired--color-{c}-foregroundon-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-950shade.What changes visually: solids keep HeroUI-style vivid fills (bright amber warning, bright green success — no more muddy darkened
-600fills), 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-variantdark: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/kunSolidBgClassesand the Button solid/shadow rows now usebg-{c} text-{c}-foreground. CheckBox, DatePicker, Switch, Carousel drop their hardcoded white/black + dark pins.
fix(vue): icon-only buttons now match the height of same-size text buttons
isIconOnlypreviously only swapped the padding (p-2.5etc.), so an icon-only button collapsed to the icon's1emheight instead of the text line-height — leaving it ~8px shorter than a text button of the samesizeand breaking alignment in a toolbar row. Icon-only buttons are now a fixed square whose side equals the same-size text-button height (the newkunControlSquareClasses), matching how shadcn/HeroUI/Chakra/Ant size their icon buttons. The icon stays at its natural1em, centered.Also pins
KunPagination's prev/next arrows tosize="sm"so they line up with the (alreadysm) numbered page buttons — without it the now-correct defaultmdicon button would render 4px taller than the numbers.
chore: ship CHANGELOG.md in the published packages
CHANGELOG.mdis now included in each package's npm tarball (added tofiles), 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.)
fix(vue): Accordion duplicate ids + Carousel dot indicators
Two issues found reviewing the 1.6.0 components:
- KunAccordion: header/panel ARIA ids were derived from the item
value, so two accordions reusing the same values (e.g.a/b) emitted duplicate ids — invalid HTML and a brokenaria-controlstarget. Ids are now generated with Vue's SSR-stableuseId(), so they're globally unique regardless ofvalue. (namestays as an optional readable prefix.) - KunCarousel: with
slidesPerView > 1the dots rendered one-per-slide, but the lastslidesPerView − 1of them could never become active. Dots now map to reachable scroll positions (maxIndex + 1), so every dot works. The dots also switched from an incorrectrole="tab"(with no tabpanels) to plain buttons witharia-current, and an internal computed no longer shadows theshowArrowsprop.
- KunAccordion: header/panel ARIA ids were derived from the item
feat(vue): five new components — Accordion, Carousel, Skeleton, Steps, Timeline
Adds the components the kungal apps were repeatedly hand-rolling on top of KunUI. All are SSR-safe and accessible, and reuse the shared design tokens / contrast helpers.
- KunAccordion + KunAccordionItem — collapsible sections. Single-open by
default or
multiple; controlled viav-model(string / string[]) or uncontrolled fromdefaultValue.light/bordered/splittedvariants. The reveal uses the CSS grid0fr → 1frtrick — animates real height with no JS measurement and renders collapsed in SSR HTML (no hydration flash). Properaria-expanded/aria-controls, and the closed panel isinert. - KunCarousel + KunCarouselItem — horizontal slider on native CSS
scroll-snap, so touch swipe + momentum work with zero JS and it renders
server-side. Prev/next arrows, dot indicators (read from scroll position) and
optional
autoplayare progressive enhancements; autoplay pauses on hover/focus and is off under reduced-motion.slidesPerViewfor thumbnail strips. - KunSkeleton — content loading placeholder (
text/rect/circle),loadedswaps in the real content via the default slot, pulse respects reduced-motion. - KunSteps — multi-step indicator (
items+current), horizontal / vertical, done / active / pending states, contrast-correct filled markers. - KunTimeline + KunTimelineItem — vertical timeline with coloured dots or icon medallions; the connecting line is pure CSS.
- KunAccordion + KunAccordionItem — collapsible sections. Single-open by
default or
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:- 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 (thesolidInfoinfocallout was essentially invisible, white on near-white). - 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 adark: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/infosolid components now use dark text instead of (illegible) white.- The dark color scale is inverted, so a plain
fix(vue): keep
borderedvariants the same size as the others (Info, TagInput)A
borderedvariant adds a real border, which enlarges the element unless the other variants reserve the same width with a transparent border. Button / Chip (via the shared variant matrix) and Tab already did this; Info and TagInput did not, so theirborderedvariant was ~2–3px larger thansolid/light/flat.- Info: every variant now carries the same
1.5pxborder (transparent for the non-bordered ones), so switching variants no longer changes the box size. - TagInput: the wrapper always reserves a
1pxtransparent border;flatandborderedare now identical in size, and the error border is now visible on theflatvariant too (it previously had no border width to colour).
No visual change to the non-bordered variants beyond the size becoming consistent — the reserved border is transparent.
- Info: every variant now carries the same
fix(vue): SSR-safe active highlight for KunTab
The Tab active indicator was measured on the client (
offsetLeft/offsetWidth) and so was absent from server-rendered HTML — on first paint (and the whole pre-hydration window) the selected tab showed only a text-color change, with the underline/pill missing. For thesolidvariant the active tab was effectively invisible (white text on no background) until hydration.The selected tab now carries a CSS-only active highlight that renders in SSR (inline inset box-shadow for
underlined; background tint forsolid/light); the JS-measured sliding indicator takes over after the client mounts, with no hydration mismatch. The indicator is also re-measured on mount and via aResizeObserver, so web-font swaps and container resizes no longer leave it stale.pills/borderedwere already SSR-safe and are unchanged.
KunContent: opt-in editorial prose typography + first-class code-copy & compact density.
- New opt-in stylesheet
@kungal/ui-vue/prose.css— a token-driven editorial type system for any.kun-prosecontainer (comfortable measure, modular heading scale, generous CJK-friendly leading, refined lists/blockquote/code/table/links, auto light/dark). It is a separate import on purpose: KunContent'sstyle.cssstill ships only behaviour, so downstreams that already own their own.kun-prosetypography are unaffected — they simply don't import it. - Code-block copy button is now built in. KunContent auto-injects a self-styled (token-aware, dark-mode-aware) copy button into each code block, with click-to-copy + instant icon feedback. Idempotent: a block that already carries a
.copybutton (e.g. one emitted by a Markdown pipeline) is left untouched, so it never doubles up — downstreams can drop their own copy implementations. - New
compactprop on KunContent (adds.kun-prose-compact) for tighter comment/reply streams — smaller base size, leading and spacing, full-width instead of the 40rem measure. Visual effect requires importing@kungal/ui-vue/prose.css.
Syntax highlighting remains a content-pipeline concern (not bundled); the prose styles theme plain code blocks neutrally and compose with pre-highlighted markup.
- New opt-in stylesheet
Content spoilers: the particle mask now follows the real text shape. Multi-line spoilers are masked line-by-line, and space-separated text is masked word-by-word (gaps at spaces and ragged line ends stay clear) instead of one solid block — the cover lines up with how the text actually flows. CJK / no-space text degrades naturally to per-line masking.
Word/line rectangles are measured once per layout via the Range API (never per frame), and the per-frame cost stays capped (the particle budget and tint fills are independent of text length), so animation never janks regardless of size. The markup contract is unchanged (
class="kun-spoiler kun-spoiler-hidden").
Content spoilers: reworked the click-to-reveal spoiler effect. The covered region now renders an animated dust/particle field (spawn → drift → fade → respawn) instead of a flat frosted block, and revealing dissolves the particles out as the text appears. The markup contract is unchanged (
class="kun-spoiler kun-spoiler-hidden"in trusted HTML).Under the hood it's now SSR-safe by construction (the cover is pure CSS present in the server-rendered HTML — no post-mount DOM injection, no hydration flash, and the secret stays hidden with JS disabled), the particle canvas is a pure client-side enhancement driven by one shared, fps-throttled rAF loop with off-screen spoilers paused via IntersectionObserver, and spoilers are now keyboard-accessible (
role="button", focusable, Enter/Space to reveal,aria-expanded). Respectsprefers-reduced-motion. The cover is rectangular (no rounded corners) so it lines up with the browser's text-selection highlight.
- Select / Autocomplete: fix the page jumping to the top the first time the dropdown is opened while scrolled down. The teleported list is momentarily at
(0,0)before floating-ui's first async measurement, soElement.scrollIntoView()(and a plainfocus()on the search field) scrolled the whole window to the top. The active option now scrolls within its own list container only, and Select's search-field focus uses{ preventScroll: true }.
- Lightbox: clicking the dark backdrop around the image now closes the viewer, matching the convention of every modern image viewer (and complementing the existing ESC-to-close). Clicks on the image and on the controls are unaffected, and a click that is the tail of a drag / pan / swipe no longer dismisses the viewer.
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, andKunPinInput(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-Vueapp.use(KunUI)already registered them). Their docs pages, prop tables, andllms.txtentries are added.
fix(vue): single-line audit follow-ups (menu items, UserChip, Tooltip)
After a full sweep for which components carry a single-unit label vs. flowing prose:
KunDropdown/KunContextMenumenu-item labels nowtruncate(single line + ellipsis when the menu is width-constrained) withshrink-0icons, instead of wrapping to two lines.KunUserChipname and description nowtruncate(the text column getsmin-w-0) — a long name ellipsizes on one line rather than wrapping past the avatar.KunTooltipdropped its unconditionalwhitespace-nowrapformax-w-xs: short tips still sit on one line, but a long tip now wraps inside ~20rem instead of being forced into one screen-wide line.
Prose components (Card / Modal / Alert / toast bodies, checkbox/radio/switch labels, helper & error text) intentionally keep wrapping.
fix(vue): keep button / chip / badge / tag labels on a single line
A label on these atomic components is one action/marker, not flowing prose, so it shouldn't wrap to a second line (the modern standard — shadcn's button ships
whitespace-nowrap, Material's spec keeps the label single-line). Addedwhitespace-nowraptoKunButton,KunChip,KunBadge, and the tags insideKunTagInput(KunTab already had it).KunButtonalso gets[&_svg]:shrink-0(plusshrink-0on its icon slots) so a long label never squishes the icons — the label overflows on one line instead of wrapping.
随依赖更新。
随依赖更新。
feat(vue): every text control's focus ring follows its
colorprop (defaultdefault)The focus-ring color was inconsistent across the form family: some controls tied it to their
colorprop (Input/NumberInput/CheckBox), others hardcoded a primary ring (Textarea/Select/Autocomplete/DatePicker/Pagination), and even among the first group the default differed (Input defaultedcolor: 'default'→ grey ring; NumberInput defaultedcolor: 'primary'→ blue ring). So an Input and a Textarea side by side focused in different colors.Now uniform: every text control's focus ring routes through its
colorprop, and they all default to'default'(a neutral grey ring) —color="primary"(etc.) themes it.color="success"/"danger"/… give that ring; an invalid control still overrides to a danger ring.- New
color?: KunUIColorprop onKunTextarea,KunSelect,KunAutocomplete,KunDatePicker(default'default'). KunNumberInputdefaultcolorchanged'primary'→'default'so an un-themed number input matches the rest (grey ring, was blue).KunPagination's jump input uses the neutral ring.
Also:
KunCardfooter no longer draws a top border — it's just a section spaced by the card's own gap, matching the (already borderless) header.- New
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 sharekunSelectionSizeClasses). The drift was in the peripheral bits:- Form labels now identical everywhere:
KunTextareaandKunDatePickerlabels gained thetext-default-700tint, andKunDatePickerdropped its oddmb-2for the standardmb-1. - Error messages now identical:
KunTextareaswitched fromtext-danger-600(and a<div>) to the standardtext-danger<p>;KunDatePickerandKunRadioGroupdroppedmt-2formt-1. - Chip / tag size: new
kunChipSizeClassesin@kungal/ui-coreis the single source for chip/tag pills.KunChipand the tags insideKunTagInputnow share it (and the pillrounded-fullshape), 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.
- Form labels now identical everywhere:
feat: unified elevation scale + misc token cleanups
Elevation scale — floating surfaces were assigned
shadow-md/shadow-lg/shadow-2xlad hoc, so same-kind surfaces disagreed (Select & Autocomplete option lists wereshadow-lg, but Dropdown & ContextMenu menus wereshadow-2xl; Modal had no shadow at all). New three-tier scale in@kungal/ui-tokens—--shadow-kun-sm/-md/-lg, generatingshadow-kun-sm|md|lgutilities (they compose withring-*via--tw-shadow, so a ringed toast still gets its elevation). Applied by tier:- sm — tooltips, slider value bubble
- md — popovers, dropdowns, context menus, select/autocomplete/date lists, toasts
- lg — modals (now actually elevated), drawers
Misc consistency cleanups:
- Raw Tailwind radii routed through the token scale:
KunBrand/KunNullrounded-2xl→rounded-kun-lg;KunLoadingrounded-lg→rounded-kun-md(so--kun-radius-scalenow affects them too). The darkKunLightboxviewer chrome keeps its own radii intentionally. KunNumberInputstepper buttons:disabled:opacity-40→disabled:opacity-50to match every other disabled control.
feat: route component transitions through the motion scale
Transitions hardcoded raw
duration-150/200/300and rawease-in/out/in-outthat didn't match the designed motion tokens (overlay enters were200msbut--kun-dur-baseis250ms; some controls used symmetricease-in-outwhile the rest used the asymmetricease-kun-*curves). Now unified:- New
duration-kun-fast | base | slow | exitutilities bound to--kun-dur-*(with literal fallbacks). Every component transition routes through them, so a global motion retune via the tokens actually propagates. - Mapped by role, preserving the asymmetric rhythm (enter decelerates, exit accelerates): overlay enter → base, leave → exit, hover/selection/focus micro → fast, skeleton/fade/large → slow.
- Remaining raw
ease-in-out/ease-outTailwind classes (Avatar, Input, Textarea, Progress) switched toease-kun-standard/ease-kun-out; scoped-style easings (Content, Ripple) now readvar(--ease-kun-*). The looping indeterminate progress keyframe and the dark Lightbox viewer keep their own timing.
Net effect: a single, consistent motion feel across every control. No API changes.
- New
fix(vue): Card header/footer, Tab item radius, Message elevation
- KunCard — the header slot no longer draws a
border-b. The footer dropped itsbg-default-100fill + double padding for a single hairline divider in the unifiedborder-kuntoken (-mt-3pulls it flush under the content), so it matches the rest of the UI instead of looking like a grey block. - KunTab —
solid/light/borderedtab items wererounded-kun-sm(6px), half the radius of every other control. Items (and their sliding indicator) are nowrounded-kun-md(12px, the default control radius) and the list container isrounded-kun-lg(16px), so the items nest concentrically and match the overall corner radius. - KunMessage (toast) — added
shadow-lg(and adark:ring-white/10edge) so toasts read as elevated/floating above the page instead of sitting flat with only a faint hairline ring.
- KunCard — the header slot no longer draws a
feat: KunTab
alignprop + one unified focus ring across every controlKunTab
align— newalign?: '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:
:focusvs:focus-withinvs:focus-visible, ring widths1/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) andkunFocusRingWithinClasses(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-withinand 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). UsekunFocusRingClasses/kunFocusRingWithinClasses.
No prop/API removals — purely additive plus a visual refinement of focus states.
- New
fix(vue): Tab underlined track line, FadeCard not animating, Pagination layout
- KunTab
variant="underlined"no longer draws a static full-length track border — only the sliding active indicator remains. - KunFadeCard now actually animates. Its
<Transition>previously wrapped an always-present<div>, so av-ifon the slotted element (the documented<KunFadeCard><Foo v-if="show"/></KunFadeCard>usage) never triggered enter/leave. Thev-ifnow lives on the Transition's direct child, driven by whether the slot has real content, so toggling collapses/expands (grid0fr↔1fr) and fades as intended. - KunPagination
justify-betweenis now effective: the page block and the jump-to-page block sat under conflictingmx-automargins (auto margins beatjustify-contentin flexbox), which spread them oddly. Removed, so the page controls sit at the start and the jump control at the end.
- KunTab
feat(tokens,vue): unified neutral border token (
--color-kun-border/border-kun)Every structural hairline (inputs, textareas, selects, autocomplete, date picker, cards, dividers, tabs, tooltips, popovers, dropdowns, context menus, drawers, pagination, slider tooltip, radio cards, tag input) now resolves to ONE semantic token instead of a scatter of
border-default-200/border-default/20/dark:border-default-200+ a per-componentdarkBordertoggle.- New:
--color-kun-border(defaults to thedefault-200step, so it flips light↔dark automatically) and aborder-kunutility. Retheme every border at once by overriding--color-kun-border(set it under.kun-dark-modetoo for a fixed non-flipping value). The global*border-color (opinionated base layer) now points at this token as well, so a barebordermatchesborder-kun. - Fixed:
KunDivider(and any control that used the translucentborder-default/20without a dark override) was ~half as bright as other hairlines in dark mode (L13% vs L26%); it now matches everything else (L26%). - Consistency: light mode is visually unchanged (the old
default/20-over-white already ≈default-200); dark mode now collapses to a single neutral border value across all components. - Interactive-control borders intentionally stay one step stronger (checkbox/radio
boxes
default-300, slider thumb) per common design-system practice — they are not structural hairlines. - Deprecated (no-op): the
darkBorderprop on Input/Textarea/NumberInput/ Select/Autocomplete/DatePicker/Card. Safe to remove from call sites; kept for backward compatibility. Note: an un-borderedKunCardthat relied ondarkBorderto show a dark-only border should now usebordered.
- New:
随依赖更新。
代码评审(CR)修复:针对 0.14–0.17 四批改动的真实项目缺陷。
- KunCopy / useKunCopy ——
useKunCopy此前是 fire-and-forget(返回void),KunCopy的await立即 resolve,导致剪贴板写入失败时也会错误地显示「已复制」(还和它自己弹出的失败 toast 自相矛盾)。改为useKunCopy返回Promise<boolean>(并兜底navigator.clipboard不存在的情况);KunCopy仅在真正成功时才切到 ✓ 状态。 - KunMessageItem(toast) ——
pauseTimer/resumeTimer改为幂等:mouseenter与pointerdown会同时触发暂停,此前会对同一startTime重复扣减剩余时间,使 toast 在用户悬停/触摸时提前消失(或进度条与实际计时不同步)。 - KunNumberInput —— 修复无
min/max且初始为空时,「−」按钮被错误禁用(null ?? -∞ > -∞为 false)的问题;空值现在可正常从 0 起步进。 - KunImage ——
fallbackSrc现在也响应「缓存命中即同步报错」路径(status==='error'),此前这种情况下回退图永远不会加载。 - KunContextMenu ——
immediatewatcher 在 SSR 且visible=true时不再访问document(加 typeof 守卫),避免renderToString崩溃。 - KunMessageProvider —— toast 容器标记
data-kun-overlay,使其在 Modal/Drawer 打开(背景 inert)时仍可交互(关闭按钮/滑动可用)。 - KunPinInput ——
length减小时截断内部 refs 数组,避免保留已卸载<input>的引用。
- KunCopy / useKunCopy ——
导航 / 展示 / 排版第四批升级:修零散 a11y/安全缺陷,补 Chip/Copy 能力。
修正(a11y / 安全)
- KunScrollShadow ——
aria-label此前误用了className(把 CSS 类当可访问名,读屏会念出 "mt-4 flex…");新增独立ariaLabelprop(默认 'scrollable content')。 - KunLink / KunButton ——
target="_blank"现在自动补rel="noopener noreferrer"(tabnabbing 防护)。 - KunAvatarGroup —— "+N" 溢出现在从
users.length推导(不传total也能显示);按user.id作 key;加role="group"+ 计数aria-label。 - KunDivider —— 竖向加
aria-orientation="vertical";withLabel标记为弃用(label 由默认插槽是否有内容决定)。 - KunMarkdown —— 装饰 SVG 加
aria-hidden。
升级
- KunChip —— 新增
closable(× 触发close,可移除标签)、disabled,以及start/end插槽(圆点/头像/图标)。 - KunCopy —— 复制后短暂反馈:图标切到 ✓、文案切到
copiedText(默认 '已复制')、aria-live播报,~1.5s 复位。 - KunImage —— 新增
fallbackSrc(图裂时回退,src变化时重置)。 - KunAvatar —— 头像 URL 裂图时回退到确定性 sticker。
- KunPagination —— 提供
pageHref时,上一页/下一页也渲染为可爬<a>(与数字页一致)。
Behavior(0.x minor)
KunScrollShadow的可访问名不再等于className,改为ariaLabel(默认 'scrollable content')。
- KunScrollShadow ——
反馈 / 状态层第三批升级:补齐 a11y(aria-live/role)、确认框可定制、Toast 体验。
修正(a11y / 正确性)
- KunProgress ——
variant="circle"现在带role="progressbar"+aria-valuenow/min/max(此前圆环完全没有,读屏不可知);indeterminate改为真正的不定动画(条形横扫 / 圆环旋转,此前只是静态满条);新增ariaLabel。 - KunLoading —— 加载态加
role="status"+aria-live="polite"+aria-busy,装饰图aria-hidden;新增轻量spinner变体(内联小尺寸,用打包的 spinner 图标)+size。 - KunMessage(toast) —— error / warn 现在用
role="alert"+aria-live="assertive"(打断式),info / success 仍是status/ polite(此前一律 polite,错误可能被读屏忽略)。 - AlertProvider —— 用
role="alertdialog"+aria-label(取自 title);确认按钮改为强调色(主操作)、取消按钮中性(此前取消是红色,与惯例相反);danger类型确认按钮变红。 - useRipple —— 涟漪
key改用自增计数(此前Date.now()在同毫秒多次点击会 key 冲突)。
升级
- useKunAlert —— 新增
confirmText/cancelText/type('info'|'warning'|'danger')/confirmColor(可本地化文案 + 危险确认)。 - KunModal —— 新增
role('dialog' | 'alertdialog')。 - KunMessage(toast) —— 每条 toast 加悬停显示的关闭按钮(
duration:0常驻 toast 也能手动关);每个位置最多并存 5 条(超出丢弃最旧);支持滑动关闭(触摸横向拖拽)。 - KunBadge —— 无锚点 slot 时渲染为独立内联徽标;新增
ariaLabel(如 "5 条未读")。
Behavior(0.x minor)
AlertProvider的取消按钮不再是 danger 红色,改为中性;确认按钮改为主色/按type着色。
- KunProgress ——
浮层 / 弹出层第二批升级:对标 Radix / HeroUI / Reka,修掉 Popover 与 ContextMenu 的 a11y 缺陷,统一浮层基建。
修正(a11y 缺陷)
- KunPopover —— 触发器不再被包裹层强加
role="button"+ 假aria-label="popover-trigger"(此前传<KunButton>会形成 button 套 button、真实可访问名被盖掉);现在是真正的对话框:打开时把焦点移入面板,关闭时归还给触发器,Esc 关闭。 - KunContextMenu —— 从「一堆按钮」升级为真正的 WAI-ARIA 菜单:
role="menu"/menuitem、roving tabindex、方向键 / Home / End / Enter / Esc 键盘导航、打开聚焦首项、关闭归还焦点(与 KunDropdown 一致)。 - KunModal / KunDrawer —— 多个叠加时按 Esc 只关闭栈顶那一个(此前会一次性关掉所有)。
升级
- KunModal —— 新增
size(sm/md/lg/xl/full)、scrollBehavior(inside/outside)、placement(center/top)。 - KunModal / KunDrawer —— 打开时给页面背景加
inert(比单靠aria-modal更强的隔离;辅助技术与 Tab 都无法进入背景)。 - KunTooltip / KunPopover —— 新增
showArrow指向触发器的小箭头。 - KunDropdown —— 新增首字母 type-ahead(按字母跳到对应项)。
内部重构(不破坏 API)
- 新增
useKunFloating—— 收敛 Popover / Tooltip / Dropdown / Select / Autocomplete 的 floating-ui 配置(offset/flip/shift + transform-origin + 可选 arrow),消除重复与漂移。 - 新增
useKunBackgroundInert—— 引用计数的背景inert管理器。 useKunOverlayZIndex新增isTopmost(开启顺序栈,供 Esc/背景判定栈顶)。
Breaking(0.x minor)
KunPopover触发器包裹层不再是role="button",也不再注入 tabindex —— 请传入可聚焦的触发器(如<KunButton>,常规用法不受影响);非交互触发器(纯图标/文本)需自行加tabindex。KunModal面板默认带max-w-md宽度上限(此前无上限);需要更宽的用size="lg|xl|full"。
- KunPopover —— 触发器不再被包裹层强加
表单 / 输入控件第一批升级:对标 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。- KunNumberInput —— 数字步进输入:
Accessibility + SSR-correctness sweep across the library.
- SSR-stable ids (the big one).
useKunUniqueIddeferred Vue'suseId()toonMounted, so the server HTML rendered empty ids — every<label for>/idpairing was broken on the server and changed on hydration. It now callsuseId()synchronously (Vue guarantees it's identical server/client), so KunInput / KunCheckBox / KunTextarea / etc. have correct, stable label associations in the SSR HTML. - KunModal dialog semantics. The panel was missing
role="dialog"/aria-modal="true"/ an accessible name — now added, plus anariaLabelprop. (KunDrawer already had these.) - Accessible names on icon-only buttons. KunModal & KunLoli close buttons and
KunPagination prev/next now have
aria-label(the icon itself isaria-hidden, so these announced as just "button" before). - KunPagination semantics. Wrapped in
<nav aria-label>; numbered pages getaria-label+aria-current="page"on the active page. - KunSlider keyboard (WCAG 2.1.1). The thumb now responds to Arrow keys (±step), PageUp/PageDown (±10×), Home (min) and End (max) — it was drag-only.
- KunMessage live region. Toast containers are now
role="status"aria-live="polite", so screen readers announce toasts. - KunTooltip keyboard/SR. Now shows on focus (not just hover), links its text
via
aria-describedby, and dismisses on Escape. - KunRating stars gained
aria-label(thetitlealone wasn't announced). - KunPopover dialog gained an
ariaLabelprop / accessible name.
All non-breaking. The id change improves SSR output; the new
ariaLabelprops on KunModal/KunPopover default to a generic name when omitted.- SSR-stable ids (the big one).
Make the navigational components render real, crawlable
<a href>links.Google only follows
<a href>— it doesn't click<div @click>/<button>/ programmatic navigation. Several KunUI components navigated viaconfig.navigateon a non-anchor element, so those links were invisible to crawlers. They now render a real anchor (config.linkComponent→<a>/NuxtLink), keeping the same navigation behavior (and working without JS):- KunBrand — the home/logo link was a
<div @click>; now a real<a>toto(the canonical crawl entry point). - KunPagination — new
pageHref?: (page) => stringprop. When provided, the numbered page controls render<a href>per page, so paginated content is crawlable. Without it, behaviour is unchanged (plain buttons). - KunAvatar / KunUserChip — a profile-linking avatar was a
<div @click>; now a real<a>to the user profile when there's a user to link to. KunUserChip wraps the whole chip (avatar + name) in one link so the name is anchor text, and gained anisNavigationprop (defaulttrue); the inner avatar is no longer a nested link. - KunDropdown / KunContextMenu — menu items gained an optional
href. An item withhrefrenders<a role="menuitem" href>(crawlable, for navigational menus); action items withouthrefstay<button>.
All changes are non-breaking: components without a navigation target (or pagination without
pageHref, menu items withouthref) render exactly as before. Note KunBrand and a profile-linking KunAvatar/KunUserChip now render an<a>instead of a<div>— restyle if you targeted the element by tag.- KunBrand — the home/logo link was a
SEO-first Tab panels + crawlable tab-as-route.
KunTab was a headless tab bar (it rendered
role="tab"buttons and exposed the active value, but no content). That left the SSR-SEO-critical decision — how to render and hide each section — entirely to the consumer, and the obviousv-ifchoice silently drops inactive panels from the indexable DOM. This adds a first-class, SEO-optimal content layer.New
KunTabPanel/KunTabPanels.<KunTab v-model="active" :items="items" name="product" /> <KunTabPanels v-model="active" name="product"> <KunTabPanel value="overview">…</KunTabPanel> <KunTabPanel value="specs">…</KunTabPanel> </KunTabPanels>mount(default"eager") —eagerserver-renders every panel into the HTML so search engines index all of it; inactive panels are hidden, not removed."lazy"renders on first activation then keeps (huge data, accepts the trade-off for unopened panels);"unmount"keeps only the active panel in the DOM (NOT crawlable — for heavy non-SEO widgets only).forceMountis a boolean alias foreager, familiar from Radix/Reka/MUI.- Inactive panels hide with
hidden="until-found"(hiddenStrategy, default) — they stay indexed and become reachable by in-page search (Ctrl+F), scroll-to-text fragments and deep links; thebeforematchreveal flips the active tab to match.hiddenStrategy="display"falls back todisplay:none. SSR/first paint is flash-free (acontent-visibilityplaceholder upgrades to the real attribute on the client). - Correct
role="tabpanel"+aria-labelledby/aria-controlswiring (tab ↔ panel ids derive from the tabvalue, namespaced by an optionalname).
KunTab:
hrefitems now render a real<a>(crawlable tab-as-route). Tabs withhrefpreviously rendered a<button>that navigated programmatically — invisible to crawlers. They now renderconfig.linkComponent(<a>/NuxtLink) with the href, so each tab is a discoverable URL and works without JS; with JS the click is intercepted and routed throughconfig.navigate(no double-nav). Tabs also gainedid/aria-controls(and KunTab anameprop) to pair with panels.
A unified motion system — smoother, more consistent animation across every component.
Motion tokens (@kungal/ui-tokens). One easing set + duration scale so the whole library shares a rhythm instead of each component inventing its own:
--ease-kun-standard / -out / -in / -emphasized(also exposed as Tailwindease-kun-*utilities) and--kun-dur-fast / -base / -slow / -exit. Curves are asymmetric by design — decelerate on enter, accelerate on exit — and exits run ~30% shorter than enters. The opinionated base layer now also honoursprefers-reduced-motion: reduce(WCAG 2.3.3).Killed the layout-thrashing animations (these caused visible stutter):
- KunTab indicator no longer transitions
height(it never changes between same-row tabs); it slides viatransformand only itswidthanimates. - KunFadeCard expands via the grid
0fr → 1frtrick instead ofmax-height— no moremax-h-96clipping of tall content, no per-frame height recalc. - KunMessage progress bar shrinks via
transform: scaleX(compositor) instead of animatingwidth.
Overlays retuned and made origin-aware. KunModal now fades its backdrop (opacity only) while the panel rises + scales independently; KunDrawer’s backdrop and panel are timing-matched. KunDropdown / KunSelect / KunPopover / KunDatePicker / KunContextMenu now grow out of their trigger —
transform-originfollows the floating-ui placement, so a menu that flips above its trigger correctly grows from its bottom edge. Every overlay shares theease-kun-*curves and timing.Micro-interactions. KunSlider’s thumb gains a hover/focus ring halo (it had no feedback before); KunSwitch gains a keyboard
focus-visiblering and a springier thumb settle; KunCheckBox’s check eases in with the emphasized curve.No component API changed. KunFadeCard now wraps its slot in a grid container (a DOM-structure change); if you targeted its immediate child with CSS, retarget the inner content.
- KunTab indicator no longer transitions
Make the default corner radius rounder, HeroUI-style.
The
--radius-kun-*scale grows so the default control radius lands at HeroUI's 12px (it was 8px):bucket before after sm 4px 6px md 12 ←default 12px lg 12px 16px md(every component's default) is now 12px,lg(floating panels — dropdown / context-menu / toast) is 16px, which keeps their concentric nesting exact (panel 16 = item 12 + the 4pxp-1inset). The--kun-radius-scaleruntime knob still multiplies on top, andnone/fullstill don't scale.One component needed a fix at the larger radius: KunCheckBox. Its small square box would look circular at a 12px token radius (12px ≈ half a 16–20px box), so the box now uses a proportional
35%radius — a rounded square at every size, never a circle (matching how HeroUI derives its checkbox radius). The radio-look variant stays a full circle. No other component needed a size change; pill/circle controls (chips, avatars, switch, slider) are unaffected.
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
sizeprop (xs–xl, defaultmd). 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
sizeprop. Track/thumb scale on clean steps (track 28×16 → 64×32, thumb = track height − 4);mdis the original switch size. - KunSlider gains a
sizeprop. Track 4→12px, thumb 14→28px;mdunchanged. - 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/xlvertical 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
sizeprop — no surveyed library gives them one.- New shared selection scale (
Unify form-control sizing on one shared scale, and fix the
lg/xlbutton 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:pya clean 2:1, horizontal padding growing faster than vertical so a bigger control gets wider, not flatter. - KunButton
lg/xlfixed —lgwaspx-6 py-2(3:1) andxlwaspx-8 py-2.5(3.2:1, a wide flat bar). They're nowpx-5 py-2.5andpx-6 py-3(both 2:1), so large buttons look proportional.mdis 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
sizeprop (xs–xl, defaultmd). Previously they had no size and were locked one notch tighter than buttons (px-3/p-3); their default horizontal padding is nowpx-4, matching KunButton/KunInputmd.
Pill/compact display components (KunChip, KunBadge, KunAvatar) are intentionally not part of this form-control scale and keep their compact sizing.
- New
KunCheckBox: add a gap between the box and its slotted content.
The box and its content sat as adjacent flex children with no gap, so the box's right edge touched the start of slotted content (
<KunCheckBox>分类</KunCheckBox>) — measured gap was 0. Only thelabelprop path was spaced, because that<label>carried its ownml-2; slot/v-htmlcontent had nothing. The wrapper now usesgap-2(matching KunRadioGroup) and the redundantml-2is dropped from the label, so the box→content gap is a uniform 8px whether you use thelabelprop or the default slot.
Bundle the KunLoli mascots, fix Tab icon spacing, enlarge the Null/Loading images, and align KunTagInput's tag color.
- KunLoli: the popup pulled its mascot from
/alert/{name}.webpin the consuming app's public dir, so it showed a broken image in any app that didn't ship those four files. The four mascots are now bundled as base64 webp data URIs (same zero-setup, no-network policy as the bundled icons and the KunLoading / KunNull images), so<KunLoli>works out of the box. - KunTab: tabs with both an icon and a label had no gap between them (the
size→gap map was defined but never applied), so the two were cramped together.
The gap (
gap-1/gap-1.5/gap-2by size) is now applied. - KunNull / KunLoading: the default mascot image is one size larger
(
w-60→w-72andw-72→w-80respectively). - KunTagInput: tags used a one-off color palette (`bg-primary/15
- KunLoli: the popup pulled its mascot from
text-primary-700) that read slightly off from the rest of the UI; they now use the same flat` variant every other KunUI chip uses, so a tag's color matches.
Remove the
KunFaviconcomponent.KunFaviconwas just a static, hardcoded inline SVG of the KunUI lollipop mark with no props — it carried no library value (an app that wants a logo ships its own asset, e.g. viaKunBrand'siconSrc). It's dropped from the@kungal/ui-vueexports and the@kungal/ui-nuxtauto-import list.Migration: if you were rendering
<KunFavicon />, inline your own logo SVG or<img>/KunImagepointing at your favicon asset instead.
Make every component's corner radius follow the unified Kun radius system.
Two classes of inconsistency were leaking through:
KunButton / KunCopy defaulted
roundedto'lg'(12px) while every other component defers to the globalconfig.rounded(defaultmd, 8px) — so buttons looked visibly rounder than inputs, cards and surfaces sitting next to them. And because'lg'was a prop default (neverundefined), settingconfig.roundedglobally couldn't pull buttons in line. Both now omit the default and resolve toconfig.roundedlike the rest; passroundedto override per-instance.Several components hardcoded raw Tailwind radii (
rounded-lg/rounded-md/rounded-xl/rounded) instead of therounded-kun-*tokens, so they neither shared the unified scale nor responded to the runtime--kun-radius-scaleknob. Converted to tokens (preserving each surface's pixel size and concentric nesting): KunTab (container + items + indicators), KunDropdown (panel + items), KunContextMenu (panel), KunSelect (listbox + options), KunMessage (toast card), KunPagination (page-jump input), KunRadioGroup (option row), KunTagInput (tag chip) and KunCheckBox (the box). Pill/circle elements usingrounded-fullare unchanged by design; KunLightbox's dark floating toolbars and KunLoading's mascot/overlay keep their own styling.
Net effect: one global radius for all components, all of it now driven by
config.roundedand scaled live by--kun-radius-scale.
随依赖更新。
Button/input sizing polish, a beautified checkbox, and a uniform corner radius.
- KunButton / KunInput sizes: horizontal padding now grows with size while
vertical padding stays tight (
py < px), so larger sizes get wider, not fatter — matching modern libraries (shadcnlg = px-8, HeroUI fixed heights).mdis unchanged;lg/xlare noticeably less bulky. Input vertical padding matches Button per size so the two line up in a form row. - KunCheckBox: the check is smaller (more breathing room in the box), stays centered, and scales in with a subtle pop. Cursor is now a pointer.
- Uniform corner radius: every component now defers to the single global
config.rounded(defaultmd). Removed the per-component radius overrides on KunModal / KunDrawer / KunInfo / KunPopover / KunUpload (werelg) and KunRadioGroup, so all surfaces share one radius — setconfig.roundedonce to restyle them together. (Pill/circle controls that userounded-fullare unaffected, by design.)
- KunButton / KunInput sizes: horizontal padding now grows with size while
vertical padding stays tight (
- KunTextarea: defer the first auto-grow height measurement to the next animation frame. Measuring
scrollHeightsynchronously inonMountedcould read a too-tall height before the textarea was laid out (notably a chat input that first appears on mobile), which only corrected itself on the first keystroke. The deferred measure runs after layout, so an auto-grow textarea starts at its true single-row height.
KunImage/KunImageNativenow default toloading="lazy".Previously
loadingdefaulted to unset, so the browser loaded every image eagerly. A page with many images (card grids, lists, avatars) fired them all at once and saturated the connection, starving the above-the-fold images — they filled in slowly behind a long-lingering skeleton, making the page feel stuck on images.KunImagealready reserves space (its aspect-ratio box + skeleton), so deferring off-screen images causes no layout shift and shortens the critical path.KunImageNativealso gains aloadingprop (it had none before).Opt your LCP / hero image back into eager loading:
<KunImage loading="eager" fetchpriority="high" … />— otherwise it's lazy like the rest, which can cost a little LCP for that one image.
KunAvatar: render the avatar URL exactly as given — stop deriving size variants in the component.
KunAvatar used to turn
user.avatarinto a 100px thumbnail by string-replacing the extension (.webp→-100.webp, most recently host-aware_100/-100). That baked CDN-specific URL conventions into the UI library. KunAvatar now rendersuser.avataras-is;sizeonly controls the rendered dimensions. Empty/missing avatar still falls back to a deterministic sticker.Migration (consumers now pass the exact URL to show): for small avatars pass the pre-sized thumbnail your CDN exposes (e.g. content-addressed
…/<hash>_100.webp, legacy…/avatar-100.webp); for profile/originalsizes pass the full image. Your backend already knows the image host, so resolving the URL belongs there — not in the UI.
- KunAvatar: pick the 100px-thumbnail variant separator by image family. Content-addressed image*service avatars (
…/aa/bb/<hash>.webp) expose variants with an underscore (<hash>_100.webp), while legacy path-based avatars use a hyphen (avatar-100.webp). The previous hardcoded hyphen-100404'd every new image_service avatar (blank top-bar/comment avatars after a user changed their picture). Now detects the two-level-hex hash path and uses*for those,-otherwise.
KunModal: the backdrop only dismisses when the press started on the backdrop.The overlay used a bare
@click, so pressing inside the modal (e.g. selecting text in an input), dragging the cursor onto the backdrop, and releasing there fired aclickon the backdrop and closed the modal — "I let go of the mouse and the dialog vanished". The overlay now tracks the pointer-down target and treats the click as a dismiss only when both the press and the release are on the backdrop itself.isDismissablebehaviour is unchanged.
随依赖更新。
Remove the
fadedvariant.faded(tinted fill + border) was visually almost indistinguishable fromghost, so it's been dropped fromKunUIVariant. This affects every variant consumer —KunButton,KunChip,KunDropdownandKunInfo.Migration: replace
variant="faded"withvariant="flat"(tinted fill, no border) orvariant="bordered"(visible colored border);ghoststays for the outline look it overlapped with.
Fix
KunDropdownyanking the page to the top when opened.The menu is teleported to
<body>and positioned by floating-ui's asynccomputePosition.open()focuses the menu inside anextTick, which fires before the position is committed — so the menu is still at its initialtop:0; left:0, and focusing it there scrolled the document to the top (very visible on mobile: tapping a trigger low on the page yanked the viewport up). All threefocus()calls now pass{ preventScroll: true }, so focus still lands on the menu/item (keyboard nav unchanged) without scrolling.
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, sobordered/faded/ghostshowed no border on KunButton, KunChip and KunDropdown. Every variant now carries an explicitborderwidth (transparent onsolid/light/flat/shadowso 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.
- Variant table: entries set
Fix stacked overlays: a newly-opened
KunModal/KunDrawercould render beneath an already-open one.All overlays shared a single z-index (
z-kun-modal), so when several were open the stacking fell back to DOM order — and because each overlayTeleports to<body>at its fixed template position, that order followed declaration order, not open order. Opening a second modal from inside the first (when the second is declared earlier in the template) buried the newer one.Overlays now claim an incrementing z-index on open via the new
useKunOverlayZIndexcomposable (anchored at the--z-kun-modaltoken so consumer overrides still apply; the counter resets when the last overlay closes), so the most-recently-opened overlay is always on top regardless of declaration/DOM order.useKunOverlayZIndexis exported for apps stacking their own overlays on the same layer. (KunLightboxuses a native<dialog>top layer and already stacked correctly.)
Fix
Unknown file extension ".css"crash under Nuxt SSR.KunUpload imports
vue-advanced-cropper's stylesheets, and the library build externalized those.csssubpaths — so bare `import 'vue-advanced-cropper/dist/
style.css'statements survived at the top of the publisheddist/index.js. Nuxt externalizes @kungal/ui-vuefor SSR and handed those paths straight to Node, which can't load.css` — crashing dev and production SSR for any app
that imported any Kun component (the cropper sits at the top of the barrel).
The build now bundles all imported dependency CSS into @kungal/ui-vue's single
dist/style.css (which consumers already import) and ships JS with no runtime
CSS imports; the cropper's JS stays external. No consumer changes needed.
- Export
useBodyScrollLock. The refcounted body scroll-lock composable that KunModal / KunDrawer / KunLightbox already use internally is now public, so apps can lock body scroll for their own overlays through the same shared counter (nested overlays won't unlock the body until the outermost one closes).
Bundle the default KunLoading / KunNull mascot images (base64 data URIs) — zero consumer setup, no network request, consistent with the bundled-icon policy.
KunLoading: defaultsrcis now a bundled image (previously relied on a consumer-provided/kun.webppublic asset).KunNull: default image is now bundled (previously fetched a random sticker from the KunUI CDN viagetRandomSticker()); added an optionalsrcprop to override it.- Both images now render at their natural aspect ratio instead of being forced into a square.
Settle on the
@kungal/ui-*package namespace; the four packages are versioned and released together.Fix KunIcon color inheritance and polish the loading/empty states:
- KunIcon: the inline SVG bodies paint with
currentColor, but the base layer's* { color }rule was landing on thev-html'd inner nodes and pinning them to the foreground color — sotext-*on (or above)<KunIcon>didn't actually color the icon. The inner nodes now inherit the icon's color. - KunLoading / KunNull: larger default image (
w-72/w-60) shown at its natural aspect ratio instead of being squished into a square. - KunNull: the empty-state caption is now muted (
text-default-500), and the default caption text changed to莲说这里什么都没有.
- KunIcon: the inline SVG bodies paint with
- Add npm
keywordsto every package for better discoverability on the npm registry.