v0.2.17

Changelog

Every notable change to @yuhuanowo/yunui — new components, fixes and refinements.

All notable changes to @yuhuanowo/yunui are documented here. The format follows Keep a Changelog; this project uses Semantic Versioning (pre-1.0: minor = features, patch = fixes, anything may change between 0.x releases).
Cadence: we deliberately stay on 0.2.x for a long run — releases are almost always patch bumps. A 0.3.0 / 1.0.0 bump needs explicit owner sign-off. See CLAUDE.md.

Unreleased

Fixed

  • Every overlay animation was dead. animate-in / animate-out / fade-in-0 / zoom-in-95 / slide-in-from-* come from the tailwindcss-animate plugin, which is Tailwind v3-only. Under v4 every one of those classes compiled to nothing, so dialogs, sheets, popovers, tooltips, dropdowns, the combobox and the theme/ language menus all snapped open and shut with no transition — in YunUI and in every app consuming it. The neighbouring duration-200 emitted fine, which hid the failure. Fixed by importing tw-animate-css (the v4 successor, API-compatible, a plain stylesheet rather than a JS plugin) from styles/yunui.css.
  • Semantic colour helpers had no working variants. .bg-error-soft, .text-error, .bg-accent-subtle and friends were hand-written rules inside @layer components, which Tailwind's variant machinery cannot see — so hover:bg-error-soft, hover:text-error and hover:bg-accent-muted emitted nothing. Banner dismiss buttons, session/account-row delete buttons, notification actions and the segmented-select active chip had no hover state at all. They are now declared with @utility, so every variant works. Added the missing text-accent-solid, border-accent-solid, border-accent-subtle and border-accent-muted, which consumers were already using against no definition.
  • ShinyButton's sheen never moved. @keyframes shimmer existed but no --animate-shimmer was registered, so animate-shimmer was a dead class; and the sheen layer sat at -z-10 under a relative (not isolate) parent, so it painted behind the button's own opaque fill. Both fixed.
  • CTASection and MarketingHero gained isolate, so their -z-10 radial washes cannot escape the component and vanish behind an opaque ancestor.
  • Semantic tones no longer use the raw Tailwind palette. Banner and StatCard were built on blue/amber/red/green/emerald/purple-500, which are not the same colours as --info/--warning/--error/--success — so a Banner and an Alert reading the same severity rendered as two different hues, and neither followed a brand theme. Both now route through the token helpers. StatCard gains semantic tone names (warning/success/info/error/accent); the old colour names stay as aliases, so nothing breaks.
  • <Card hover> uses the house hover. It invented its own (hover:border-ring hover:shadow-lg), contradicting the .card:hover that every raw className="card" surface gets; it now applies card-interactive.
  • Modals sit on the overlay shadow ramp. Modal, ConfirmModal and ConfirmCloseDialog used shadow-2xl — a far heavier drop than anything else in the system — and Modal used bg-background/95 where every other overlay uses bg-popover/85. All three now use the standard shadow-lg shadow-black/5 recipe.
  • Bare border no longer renders as currentColor in SegmentedSelect, ModelSelect and FeatureLockedState (they now set border-border), so the border stops shifting hue with the text colour.
  • Misc token cleanup: the offline status dot (zinc-400muted-foreground/60), ModelCard's tier pill (hand-rolled two-theme amber → bg-warning-soft text-warning), and KaTeX's error colour (hardcoded #cc0000 → the live --error token).

Fixed

  • BentoCard hover speaks YunUI's language again. It carried the stock bento-grid trick of sliding its content sideways (translate-x-2) on hover, which reads as jitter and matches nothing else in the system. It now uses the house treatment: a quiet lift, a deeper shadow, a top hairline highlight and a soft corner glow that fades in — the same "發亮" feel the landing pages use.

Performance

  • useAnchoredPosition no longer thrashes layout on scroll. An open floating panel (select / combobox / custom-select / model picker) subscribes to the capture-phase scroll of every ancestor; it now coalesces those events into one requestAnimationFrame measurement instead of running a getBoundingClientRect reflow per scroll event, and the listener is passive. Resize and the ResizeObserver stay synchronous so content changes still remeasure promptly.
  • Sidebar persists its scroll position at most once per frame. It was writing sessionStorage synchronously on every scroll event; the write is now rAF-coalesced and the listener is passive.

Changed

  • Label props follow one rule now, and it is written down. They had drifted into a shape nobody could predict: SessionItem took four separate *Label props, NotificationPanel three, and Pagination and BlogPagination — the same control twice — disagreed on whether it was previousLabel/nextLabel or labels: { previous, next }. The rule is now in CONTRIBUTING.md: label for the component's own name, ariaLabel for a container whose label already means something else, <x>Label for exactly one auxiliary string, and a labels object for two or more. One-string components are deliberately left alone — labels={{ back: "…" }} is worse than backLabel="…".
    Breaking, for four components: SessionItem currentLabel/inactiveLabel/runningLabel/revokeLabellabels.{current,inactive,running,revoke}; NotificationPanel unreadLabel/loadingLabel/emptyLabellabels.{unread,loading,empty}; Pagination previousLabel/nextLabellabels.{previous,next}; ChatComposer sendLabel/stopLabellabels.{send,stop}.

Fixed

  • Pagination announced its page buttons in English, always. Every numbered button was named with a hardcoded template literal — aria-label={`Go to page ${n}`} — with no prop behind it, so a Chinese page read out English to screen readers with no way for the host to override. It takes labels.page(n) now, defaulting to the old wording. The CONTRIBUTING grep that is supposed to catch this only matches literal attributes, which is exactly why it survived; that gap is now documented next to the check, and the rest of src/ was swept for the same shape.
  • --text-tertiary was tuned against the wrong surface in dark. It was measured against --bg-elevated (#18181b) on the reasoning that cards are harder than the page background — right as far as it went, but it missed the soft status tints (bg-accent-soft, bg-info-soft, … at #27272a), which are lighter still. At #85858e that is 4.07:1, so .text-label inside a tinted card failed AA. This only became visible once the .nav-section failure above stopped masking it. Now #91919a: 4.77 on the tints / 5.67 on elevated / 6.37 on base, still clearly below --text-secondary so the three-step hierarchy survives. Light is unchanged — its soft tint is #f4f4f5, where the existing value already measures 4.81:1.
  • --color-fd-muted-foreground had no bridge in light or dark either. Only true-black overrode it, so the other two themes inherited fumadocs' own #737373 — which clears AA on pure white (4.74:1) and fails on every surface it actually lands on: 4.39:1 on #f6f6f6, 4.35 on #f5f5f5, 4.20 on #f1f1f1. Now bridged to --text-tertiary in all themes (4.89 / 4.84 / 4.68, 5.28 on white). Found on Yunxin's docs, whose surfaces are slightly grey; YunUI's own site sits on white and never showed it.
  • The whole true-black fumadocs bridge was losing the cascade. Fumadocs ships its LIGHT token defaults as :root:not(.dark) unlayered. true-black sets class="true-black" with no dark, so that selector matches it — and unlayered CSS beats every @layer regardless of specificity, so the .true-black bridge in @layer components never applied. Docs body text rendered at fumadocs' light #525252 on black: 2.53:1. Moving it out of the layer was necessary but not sufficient: :root:not(.dark) is (0,2,0) and a bare .true-black is (0,1,0), so it still lost. The bridge is now :root.true-black:not(.dark) at (0,3,0), unlayered, deliberately one step above. Found by sweeping accessibility in true-black for the first time — see site/visual/axe-theme.mjs.
  • Code blocks rendered the LIGHT syntax theme in the true-black theme — 1.35:1. Fumadocs emits --shiki-light / --shiki-dark per token and flips them with .dark .shiki … code span. next-themes maps one theme to one class, so true-black sets class="true-black" with no dark beside it and that switch never fired: GitHub's light palette on #0a0a0a. A .true-black counterpart existed but sat in @layer components, where fumadocs' own light rule in @layer utilities beat it — later layer wins, specificity is not consulted. It is unlayered now (a marked section at the end of yunui.css), and covers both DOM shapes fumadocs emits (.shiki on the wrapping figure, and on the <code> itself). Measured worst token after the fix: 7.45:1.
  • CodeBlock line numbers were invisible when you hovered to read them. --text-muted (2.57:1 in dark before opacity) at group-hover:opacity-50 works out to roughly 1.4:1. They still only appear on hover, but now in --text-tertiary at full opacity: 6.37:1 dark / 6.72:1 true-black / 5.28:1 light.
  • --color-fd-card was bridged only in true-black. Light and dark used fumadocs' own #f1f1f1 / #191919 while every YunUI card beside them used --bg-card — two card colours on one page, which is what the fd-* bridge exists to prevent. It also cost contrast: fumadocs renders code blocks on this surface, and GitHub's palettes are calibrated against their own canvas, so the light comment token measured 4.03:1 on #f1f1f1 and 4.55:1 on white. Every intermediate grey still failed, so it was white or nothing.
  • .nav-section failed AA on every page that has a sidebar. The uppercase 10px section headings ("PLAYGROUND", "GENERATE", …) used --text-muted, which in dark is #52525b on #09090b — 2.57:1, against a 4.5:1 requirement that 10px uppercase gets no allowance from. The same correction had already been made twice in this stylesheet, with the same wording in the comment; this was the third instance and it was missed. Now --text-tertiary, measured at 5.44:1. axe flagged it on all 23 Yunshu routes.
  • Modal's focus trap never armed, and the trap leaked. Two defects, found by driving a real dialog in Chromium and WebKit rather than by reading the hook. useFocusTrap bailed on if (!container) return with deps of enabled plus a ref object whose identity never changes — so once it bailed it never ran again, and portal dialogs render null until their SSR-safety mounted state is set, so the container arrives a commit later. Sheet, ConfirmModal and ConfirmCloseDialog pass && mounted; Modal passed bare isOpen and its trap silently did nothing — a dialog opened with focus still on the button behind it. Separately the keydown listener was bound to the container, which only fires for keys pressed while focus is already inside — the one case a trap does not need to handle — so Tab from outside walked straight past it. It is on document in the capture phase now, and the hook retries on the next frame instead of giving up, so the next component to get this wrong degrades to a frame of delay rather than to silence.
  • CodeDemo was hardcoded to one gateway. All three snippets baked in https://api.example.com/v1 and deepseek-r1, and the docs stated it "takes no props" — so any app but the original rendered a landing page telling readers to call example.com. baseUrl, model, apiKeyPlaceholder, prompt, labels and className are props now, defaulting to the previous literals so rendered output is unchanged until you pass something. Values are interpolated through quote escaping, so a prompt containing a quote can't break the snippet.

Infrastructure

  • CI gates on three things a typecheck cannot see. dist/ must reproduce from src/ (consumers install by git pin, so the committed dist/ is the package and could drift silently); scripts/check-emitted-css.mjs asserts that every literal className in src/ produces a selector in the built stylesheet — the failure mode behind bg-error/10, hover:bg-error-soft and the dead tailwindcss-animate classes, all of which look perfectly ordinary in review — and also fails on fd-* leakage from fumadocs; and the axe sweep now exits non-zero. Visual regression stays a local tool (pnpm test:visual) and is chromium-only: WebKit could not hold a baseline in the Playwright container, flaking on three or four of twelve tests per run even at a 900 px budget.

[0.2.17] - 2026-08-06

Added

  • ModelSelect gains renderHeader + filterResetKeyrenderHeader renders a caller-owned toolbar pinned to the TOP of the open panel, always visible even when the result list is empty (unlike renderFooter, which hides on an empty set) — the place for a domain control that must stay reachable, e.g. a browse-mode switch above the picker. filterResetKey imperatively clears the internal search + provider/capability filters when its value changes, WITHOUT remounting, so the panel keeps its open state/scroll/focus and never flashes a stale-filter "no results" after the option set is swapped out from under the filters (again: a browse-mode switch). Both are optional and fully backward-compatible.

Fixed

  • MermaidDiagram restores its documented .mermaid-container DOM contract. The content stylesheet already scoped responsive SVG rules to that class, but the rendered diagram omitted it, leaving host styling and stable diagram selection disconnected from the actual output.
  • CodeBlock no longer pulls Shiki's full language catalogue into host builds. The content renderer now uses Shiki core with explicit lazy grammar/theme loaders and the JavaScript regex engine. Common application and LLM-output languages retain VS Code-grade highlighting; unknown fence labels fall back to escaped plaintext. This keeps MarkdownRenderer on demand in practice, instead of making Next/Vite discover and compile hundreds of unused language modules.
  • Overlays are no longer see-through — dropdown menus, selects, comboboxes, popovers, the language & theme switchers, the mobile navbar sheet and the notification panel were painted with a 60%-opaque bg-background/60 frosted panel, so content behind them showed through and read as if it were still clickable. They now use bg-popover/85 (still frosted via backdrop-blur, but opaque enough to occlude what's underneath) and the correct --popover surface token.
  • Button labels never wrap — the .btn base now sets white-space: nowrap, so a button label can no longer break onto multiple lines in a narrow container. A CJK label like 使用 in a cramped table action column used to split vertically into 使/; it now stays on one line (the column widens / the table scrolls instead). Matches the standard button convention.

Added

  • Sortable TableHeadTableHead gains optional, backward-compatible sort props: onSort makes the header a sort control (renders its content as a <button> with an asc/desc/unsorted chevron and sets aria-sort on the <th>), sortDirection ("asc" | "desc" | false) is the column's current state, and align ("left" | "right" | "center") aligns both the content and the control (use "right" for numeric columns). Omit onSort and it behaves exactly as before. Fills the data-table column-sorting gap without a separate component. TableHead is also now whitespace-nowrap so short header labels never wrap — CJK labels in particular no longer break mid-word (能力 → 能/力) in narrow columns / WebKit.
  • AgentTimeline (@yuhuanowo/yunui/ai) — an agent turn rendered as an ordered, INLINE sequence of typed blocks (reasoning / tool call+result / assistant text / approval), in the neutral .card language. Unlike the AgentSteps panel, the answer is a text block among the tool/reasoning blocks in conversation order (no result-on-top / steps-below split); tool rows expand into $-prefixed monospace output, failures use semantic red-500/5 tints, and an approval block renders an amber allow/deny gate. Prop-driven and copy-free — the consumer maps records onto AgentTimelineBlock, localizes every label, and supplies a markdown renderer. Verified on Chrome + Safari/WebKit, desktop + mobile.
  • AgentRunStatus (@yuhuanowo/yunui/ai) — a layout-stable live surface for the single activity an agent is doing now (a shimmering label + pulsing dot trio, announced via role="status"/aria-live). Complements AgentTimeline: the status stays put for the whole run while timeline rows become durable history. phase (thinking/acting/observing/reflecting/responding/waiting) is a semantic hint.
  • TextShimmer (@yuhuanowo/yunui) — a restrained animated text sweep for transient work states ("Thinking…"); the label is exposed to assistive tech exactly once while the duplicated paint layers stay aria-hidden. active pauses the sweep without a layout shift.
  • ChatAttachment (@yuhuanowo/yunui/chat) — a compact attachment surface shared by the composer and sent messages: name/meta, an optional preview or icon, a determinate upload progress bar, status (idle/loading/error) affordances and an actions slot.
  • InlineCitation (@yuhuanowo/yunui/content) — a claim-adjacent evidence marker for grounded AI output. A compact inline pill previews the concrete source (title, location, a bounded excerpt) on hover/focus and delegates opening to the host via onOpen.
  • ReadingProgress (@yuhuanowo/yunui/patterns) — reading position + back-to-top as one floating control: a circular ring fills with scroll progress around an up-arrow button (no full-width hairline). bar/backToTop toggle each half; threshold and labels.backToTop are configurable. One rAF-coalesced scroll listener; the ring animates via stroke-dashoffset.
  • SettingsShell (@yuhuanowo/yunui/patterns) — the canonical settings layout: grouped desktop sidebar navigation, a compact mobile Select, and one scroll-safe content lane; works in a dialog or full page. Controlled via value/onValueChange, sections from groups (SettingsNavGroup/SettingsNavItem). The host owns the active panel; YunUI owns navigation.
  • NavStateIndicator (@yuhuanowo/yunui/patterns) — the shared active/running marker that sits beside a selected item in Sidebar/SettingsShell; a presentational <span> driven by active/running data-states. Extracted so bespoke navigation surfaces can match the system.
  • CustomSelect server-backed search + infinite scrollonSearch (debounced by searchDebounceMs, default 250) drives results from a backend, paired with loading; onLoadMore + hasMore append pages as the list nears its end. Both optional and independent of the existing client-side searchable filtering.
  • Combobox gains clearable (default true) — a clear (×) action for the current value; set clearable={false} when the field must always hold a value.
  • useAnchoredPosition flips above the trigger when there isn't room below, and remeasures open panels as layout changes — hand-rolled floating panels now match the collision behavior of the Radix-based overlays.
  • getModelDeveloperId (@yuhuanowo/yunui/ai) — model-family icon resolution is centralized, so a model id resolves to its developer/family icon through one shared helper.

Fixed

  • Untrusted Mermaid/KaTeX is sanitizedMermaidDiagram renders with a strict securityLevel and KaTeX with trust: false, so diagram/math content coming from model or user input can't inject scripts or navigate via crafted labels (XSS hardening).
  • Tooltip content escapes clipping ancestors — tooltips now portal out, so they're no longer cut off by an overflow: hidden/clip parent.

Removed

  • AgentSteps (@yuhuanowo/yunui/ai) — the earlier Codex-style execution panel (result-on-top / steps-below split) is removed, superseded by AgentTimeline, which renders the same agent-turn data as an inline ordered sequence of typed blocks. No consumer shipped against AgentSteps.

Documentation

  • The library's non-component API had no documentation at all. Ten hooks, the whole runtime theming API, cn, the four date formatters, the provider-icon helpers and the capability colour map were exported and mentioned nowhere in the docs corpus — a consumer could not discover useDismissOnOutside, applyTheme or getIconPath short of reading dist. Three new pages, in all three locales: Hooks, Theming and Utilities.
  • useAnchoredPosition and useScrollableTabStop are now exported. Both were internal, and both solve problems consumers hit on their own — hand-rolled dropdowns running off the bottom of a phone, and scroll containers Safari and Firefox will not focus.
  • Filled the last four naming gaps: useYunUI (on Hooks), Tfoot (Table), ConfirmCloseDialog (Confirm Modal) and ProviderIconImg (Provider Icon).
  • The fourteen compound sub-exports are documented. CardHeader/CardTitle/ CardDescription/CardContent/CardFooter, SelectLabel/SelectSeparator/ SelectScrollUpButton/SelectScrollDownButton, DropdownMenuGroup/ DropdownMenuPortal and the fadeIn/staggerContainer/staggerItem variant presets were all exported from the barrel and appeared nowhere in the docs corpus — not even named on their family pages. Now on Card, Select, DropdownMenu and Motion, in all three locales, with a live Card-parts preview.
  • The "components carry no copy" rule now says what the code actually does. The library ships English defaults behind overridable props (labels, closeLabel, …), which is a different and defensible rule — but CLAUDE.md and CONTRIBUTING.md claimed zero copy, so the two could never both be true. Both now state the real contract, with the grep that checks it. That grep currently returns nothing: no bare aria-label="…" literals remain in src/.

Fixed

  • The syntax theme failed AA in both modes. github-light's constant colour (#e36209) measures 3.49:1 and github-dark's comment colour (#6a737d) 3.93:1, each against its own background — both text, both under the 4.5:1 floor. Swapped to the -default variants GitHub itself moved to, which have no failing text token in either mode and are visually near-identical.
  • A YunUI Checkbox could not be given an accessible name at all. It renders a <button role="checkbox">, so <label> — wrapping or htmlFor — does not name it; and the component dropped every prop it did not explicitly list, so aria-label was silently discarded too. Between the two there was no way to name one. It forwards its props now, the doc comment no longer recommends the method that cannot work, and three tests pin it.
  • Six components carried unnamed ARIA roles. Switch (role="switch"), Progress, Gauge, MetricBar and ChatAttachment's upload bar (role="progressbar"), and Slider's thumb (role="slider") all rendered without an accessible name — announced as an unlabelled control, or a bare percentage with no idea what it measures. Each takes a label now (MetricBar reuses its visible one).
  • TextShimmer announced nothing. It put aria-label on a role-less <span>, which ARIA prohibits and browsers ignore, while both paint layers were aria-hidden — so "Thinking…" was invisible to a screen reader. It renders a real sr-only copy now. Same for the markdown task-list checkbox, which was an unlabelled readonly control and is now correctly hidden.
  • SegmentedBar and Sparkline claimed role="img" with no name, telling a screen reader there is a picture and nothing about it. The role is now conditional on a label.
  • Pagination's Previous/Next had no name on a phone. The word is hidden sm:inline, which left a bare chevron below sm — in both BlogPagination and SimplePagination.
  • Tinted badges failed AA. The capability pills, FellowBadge, SourceBadge and the capability selector used -600 ink on a 10% tint of the same hue: amber measured 2.96:1, cyan 3.29:1, pink 3.98:1 against the 4.5:1 floor. Light mode is -700 now; dark keeps -400. Meaningful glyphs moved -500-600 for SC 1.4.11's 3:1. .badge-info and StatCard's toned labels likewise.
  • Nested frosted glass never worked, and the code blamed the wrong thing. The mobile menu inside Navbar carried bg-popover/95 backdrop-blur-2xl, but the bar it is nested in has its own backdrop-filter — which makes that bar a backdrop root, so a nested backdrop-filter samples an empty backdrop and renders nothing. Proven by measurement: forcing the panel's backdrop-filter to none produced a byte-identical screenshot, while removing the bar's changed it. The 95% fill meant the page read straight through the open menu — the hero headline was legible behind it. The panel is opaque now, with the dead blur removed. The Navbar comment that attributed this to transform has been corrected.
  • Three more inputs were 14px on mobile, so iOS Safari zoomed the page on focus — and two of them (Combobox, CustomSelect) open inside a dropdown, so the zoom landed mid-interaction. ModelSelect's search too. All now text-base md:text-sm, matching the earlier fix to Input/Textarea/etc.
  • LLMCopyButton, ViewOptions and buttonVariants shipped unstyled. They were written against fd-* utilities, but the --color-fd-* variables are declared only inside YunUI's true-black scope, never in @theme — so Tailwind generates no bg-fd-primary / hover:bg-fd-accent / ring-fd-ring at all in a consumer that has not separately installed fumadocs' theme. Confirmed absent from YunNEWS's built stylesheet. Rewritten onto YunUI's own registered colours.
  • Table's scroll wrapper is now keyboard-reachable when it overflows. Chrome makes overflowing scroll containers focusable on its own; Safari and Firefox do not, so a wide table of plain cells had no keyboard route to the columns past the right edge. New useScrollableTabStop measures actual overflow, so tables that fit gain no dead tab stop, and scrollLabel names the region when it does become focusable.
  • Live docs previews linked to routes this site does not have — the Footer preview offered /models, /pricing, /about, /blog, /careers; Navbar /models and /pricing; LinkRow /help; Notification /notifications; AccountMenu /login. All eight 404'd when clicked. Demo links now point at #.
  • /changelog had two <main> landmarks — the marketing shell already wraps every page in one.

Added

  • Navbar gained the slots its two forks needed. YunUI's own marketing site and YunNEWS each kept a private copy of this bar — not out of reluctance, but because the API could not express "a mark-only brand" or "a GitHub link / a search button in the right-hand cluster". New: brand, actions, mobileMenuHeader, mobileMenuFooter, label (the <nav> accessible name) and className. mobileMenuFooter deliberately replaces the automatic language row in the mobile menu, so a host can lay that row out itself instead of getting two. YunUI's site fork is retired in this release.
  • MembershipCard — the Fellows membership card, extracted class for class from Yunxin, which was the one place it existed.
  • CommandPalette rows can be real links. Give an item an href and the row renders as an anchor through the adapter's Link instead of a <button>, so ⌘-click, middle-click and "open in new tab" work. A search result is a link; losing that meant a reader checking three results had to reopen the palette between each one. Also gained a footer slot (result counts / keyboard legend), a spinner in the input row while loading keeps the previous results on screen, and proper role="listbox" / role="option" / aria-activedescendant wiring so the highlighted row is announced.
  • useDismissOnOutside — one hook replacing six hand-rolled copies of "close this panel when the user presses outside it" (CustomSelect, Combobox, ThemeToggle, LanguageSwitcher, ModelSelect, AccountMenu). No two agreed: four registered their listener permanently, so it ran on every click in the host app whether the panel was open or not, and four listened for mousedown only — meaning on a phone, tapping away did not close them. The hook takes the correct half of each: listeners exist only while open, and cover mouse and touch.
  • PageLayout now puts id="main-content" on its <main> (overridable via mainId). Yunxin's root layout ships a "Skip to main content" link on every page, but #main-content only existed in the dashboard layout — so on the homepage, /models, /fellows, /about, /blog and /contact the skip link pointed at nothing at all. Those pages all route through PageLayout, so this fixes them without touching Yunxin.
  • Docs for the nine components that had none. Kbd, Steps, Separator, AvatarGroup, MotionDiv/MotionSpan, NumberInput, PasswordInput and SearchInput were exported from the barrel but appeared nowhere in the docs site — no page, no sidebar entry, not in search. Eight new pages (Motion covers both re-exports) in all three locales, with live previews and props tables.

Changed

  • Dropped two dead dependencies, @radix-ui/react-presence and @radix-ui/react-switch. Neither is referenced anywhere in the source or the build — Switch is hand-rolled on a plain <button> — so they were pure install weight for every consumer.
  • AuthShell is now a faithful extraction of Yunxin's auth screens. It had been written as an improvement on them — the panel used the house .card (a 20px radius with a shadow and a hover transition) where all nine Yunxin auth screens use a flat p-6 bg-card border border-border rounded-xl. Adopting it would therefore have silently restyled sign-in, sign-up, forgot/reset password, verify-email, resend-verification and the OAuth callback. Yunxin is the original and stays the reference: the shell now reproduces its markup class for class, and a test pins those strings so they cannot drift. Gained the error, icon and centered slots those screens actually use.
  • The last hardcoded English strings are now overridable. The library is meant to carry no copy at all, but DialogContent and Sheet shipped a literal aria-label="Close", Combobox shipped "Clear" / "Toggle options", and LLMCopyButton / ViewOptions shipped six visible labels. All are now props (closeLabel, labels), with the English text kept only as a default so the components stay usable untranslated.
  • ModelSelect selection bar inset refined to 5px (from left-1.5/6px), set as an inline style since Tailwind's 1.25 step isn't core and could be dropped by a consumer's JIT scan.
  • AgentTimeline now speaks YunUI's semantic status vocabulary — the error / success / warning affordances moved off raw Tailwind red-*/emerald-*/amber-* onto the --error / --success / --warning tokens, so they track the active theme (light / dark / brand) instead of drifting from the system's palette.
  • ./package.json is now an export so tooling and consumers can read the package version at runtime (the docs site uses it for its version badge).
  • Dependencies refreshed to latest — React 19.2.8, the Radix primitives, Tailwind 4.3.3, shiki 4, framer-motion 12.43, lucide-react 1.28 and more. No API changes; typescript intentionally held at 6.x (7.x isn't yet compatible with the tsup/rollup-plugin-dts build toolchain).

[0.2.16] - 2026-07-02

Fixed

  • ModelSelect selection bar sat flush against the row edge. The selected / hover accent bar was left-0; inset to left-1.5 so it no longer crowds the panel edge next to the model icon.
  • ChatMessageList auto-scroll hijacked the whole document. Its stick-to-bottom used endRef.scrollIntoView(), which scrolls every scrollable ancestor — including the window. On a full-height chat page it was invisible, but when the list is one section of a long page (e.g. the showcase) the entire window jumped down to the chat on mount. Now it pins only its own overflow container (scrollTop = scrollHeight) and never touches the document scroll; internal stick-to-bottom is unchanged.
  • MediaGallery grid hover actions were near-invisible, and images had no built-in zoom. The download/delete controls used page-level outline button variants (primary = near-black icon on a transparent fill, destructive = coral outline) rendered on top of the image + hover scrim, so they read as empty outlines with no visible glyph. They are now solid, theme-aware circular controls (card fill + ring + shadow, foreground/error icon) that stay legible on any image in light and dark. Completed images are also click-to-zoom by default via the same ImageLightbox that ContentImage uses (zoom / rotate / download / keyboard); a host onPreview still overrides it.
  • ModelManagerCard no longer looks top-heavy when it has actions but no row-select control. The top control bar (min-h-7 mb-3) rendered whenever selectSlot OR actions existed, so a card with only actions (no multi-select checkbox) floated the action button alone top-right above a dead band, with the icon/name pushed to a second row. The bar now renders only for selectSlot; without it the actions ride on the identity row (right-aligned, aligned with the name). Multi-select admin mode (with selectSlot) is unchanged.
  • Sheet body now has default padding (px-5 py-4, matching its header). Without it every consumer's sheet content sat flush against the panel edges (e.g. settings drawers with sliders touching the border).
  • ThemeToggle pill no longer shows a lopsided blank stretch in dark mode. The Sun stayed in-flow while scale-0 (still occupying layout) and the Moon was absolutely positioned against the button, so dark mode rendered the icon off-center beside an empty slot. Both icons now stack in one 14px slot; the pre-mount skeleton also shrank from w-16 to the trigger's real footprint.
  • TabsList gained max-w-full — as an inline-flex it sized to content, so a long tab strip dragged the page sideways on narrow screens instead of engaging its own overflow-x-auto.
  • Scrollbar thumb is token-based (--border-default, hover --border-strong) instead of black-alpha, which was invisible on dark and true-black backgrounds.
  • Dark-mode readability of the shadcn muted bridge. --color-muted-foreground and the .text-label utility mapped to --text-muted (#52525b in dark) — only 2.57:1, so every consumer label/caption/section-header that used text-muted-foreground (the shadcn convention for readable secondary text) washed out. Both now map to --text-tertiary (#71717a, ~4:1). Light mode is unchanged (its --text-muted already equals --text-tertiary). Fixes the app-wide "washed-out details" seen across consumer surfaces.
  • Gauge / Slider tracks were invisible in dark mode. Both used --color-muted (= --bg-elevated) for the unfilled track, which blends into dark cards — the gauge ring and slider trough effectively disappeared. Now use --color-border.
  • Slider thumb no longer clips at the track extremes. The Root gained px-2.5 (half the 20px thumb) so a thumb at min/max stays inside its box instead of overflowing/clipping past the panel edge — very visible in narrow containers (e.g. a settings drawer on mobile).
  • TabsTrigger icon + label spacing. Added gap-1.5 so an icon child no longer sits flush against the label text.

Changed

  • Content + chat stacks fully restyled in YunUI's flat design vocabulary. Follow-up to the earlier token pass: every src/content/* and src/chat/* component now uses the flat design tokens and class API that the rest of YunUI (patterns / ai) uses — --bg-elevated / --bg-card / --border-hairline / --border-default / --text-primary·secondary·tertiary / --accent and the .card / .badge / *-soft classes — instead of the shadcn @theme aliases (bg-muted / border-border / text-muted-foreground / bg-primary) it was lifted from Agent with. This guarantees the content/chat surfaces match YunUI even inside consumer apps that define their own shadcn tokens.
  • CodeBlock rewritten to YunUI's original code-block design — the .card container, window-chrome traffic-light dots, a language .badge, --bg-elevated header / --bg-base body and --accent-subtle hover buttons, matching patterns/CodeBlock (keeps Shiki highlighting). Was a divergent terminal-header design lifted from Agent.
  • MarkdownRenderer root is not-prose — it styles every element itself, so a host .prose (Tailwind Typography) was double-styling the output (a second table frame + dead margins, shoved callout titles, code that looked different inside vs outside prose). The custom component map is now the single source of truth, immune to host prose.
  • Markdown links carry a persistent underline (subtle --border-default, darkening on hover) so links read as links, not body text.
  • Content stack now speaks YunUI's design language. CalloutBlock maps its GitHub admonition tones onto the semantic token utilities (bg-info-soft / text-success / border-error-soft, "important" → accent tokens) instead of hard-coded palette colors, so callouts re-theme with the design system. CodeBlock and MarkdownRenderer drop raw bg-[#f6f8fa] / bg-green-100 / bg-yellow-200 for bg-muted / bg-success-soft / bg-warning-soft.

Fixed

  • Markdown callouts (> [!NOTE]) rendered as a raw blockquote with the marker text showing — react-markdown emits a leading "\n" text node so the paragraph was never the first child. The blockquote handler now finds the first real element and preserves inline nodes, so callouts become CalloutBlock (which composes Alert).
  • Shiki code was double-spaced — block .line spans separated by literal "\n" text nodes rendered an extra empty line under white-space: pre. content.css collapses the inter-line newlines while each .line keeps pre.
  • Emphasized code lines (highlightLines) were clipped and off-brand — the highlight used indigo and, on horizontal scroll, only spanned the visible width (cutting mid-token). Now tinted with the host --accent (indigo fallback) and code is sized to its widest line so the highlight covers the whole row.
  • Task-list checkboxes rendered browser-grey — a native checkbox tick is colored by CSS accent-color, not text-*; switched to accent-(--accent).
  • Code blocks drew a nested "box-in-box" — a leaked host/prose code style (border + radius + chip background) bled into Shiki's <code>. content.css now neutralizes it, so code sits flat on one clean surface.
  • Touch a11y — code-block copy/edit buttons and ChatMessage hover actions were opacity-0 group-hover (invisible/unreachable on touch); now visible on coarse pointers (opacity-100 md:opacity-0 md:group-hover:opacity-100).
  • iOS focus-zoomChatComposer's textarea was text-sm (<16px); now text-base md:text-sm so mobile Safari doesn't zoom on focus.
  • Aligned content surfaces to YunUI's radius language (rounded-lgrounded-xl on callouts, code blocks, tables, details, mermaid, images), the composer focus state to the ring convention, and the last raw colors (border-gray-300, bg-destructive/10) to semantic utilities.
  • Content stack infinite render loopMermaidDiagram (stuck on "Rendering diagram…") and MathRenderer (CPU thrash) looped forever because useContentT() and the default adapter's useT returned a fresh function on every render, which was in those components' effect deps. useT/useContentT now return stable references, and the async effects no longer depend on the translator. Only surfaced under real rendering — caught by screenshot QA.
  • ChatMessage header collided badges with the timestamp on narrow widths — a flex items-center header meant wrapping badges (e.g. a model chip plus a GenerationStats token/throughput/latency row) overlapped the right-aligned timestamp on mobile. The header now aligns to the top, wraps the name/badges group, and keeps the timestamp on the first line — clean at every width.
  • ContentImage spun forever when a ChatMessageList shared the page — the lazy-load IntersectionObserver rooted itself at the first [data-scroll-container="true"] element (which ChatMessageList sets). If the image sat outside that list (the normal content + chat page), it never intersected, so the src was never assigned and the spinner never resolved. The observer now only uses that container as root when it actually contains the image, else falls back to the viewport. Caught by screenshot QA on the showcase.

Added

  • MermaidDiagram is hand-drawn (Excalidraw-style) for every diagram type. Uses Mermaid 11's native look: "handDrawn" for the types it supports (flowchart, state, class, ER…) and, for the ones it leaves straight (sequence, gantt, pie, journey, gitGraph, timeline, mindmap…), post-processes the SVG with a turbulence-displacement filter applied to strokes/shapes only (text stays crisp), so all diagrams read as a sketch.
  • MermaidDiagram click-to-zoom — new enableZoom (default true) opens the diagram in a full-screen lightbox on a theme-matched panel (readable in light and dark). ImageLightbox now accepts arbitrary children (not just an <img src>), so any inline SVG can reuse its zoom/rotate/close controls.
  • Localizable labels for the last hardcoded stringsAreaChart noDataLabel, AudioPlayer labels (play/pause/seek/download aria), and ChatComposer sendLabel / stopLabel, so every user-facing string in the new components is host-overridable.
  • ChatComposer allowSendEmpty — allow sending with an empty textarea (e.g. when attachments alone form a valid message).
  • ScrollArea / ScrollBar (primitives) — a styled, cross-browser custom scrollbar around any overflowing content (Radix-based). Adds @radix-ui/react-scroll-area.
  • Card sub-componentsCardHeader, CardTitle, CardDescription, CardContent, CardFooter for composing structured cards (previously Card was a single container).

Docs / site

  • Showcase Content + Chat sections are now localized. They were the only sections with hardcoded English section/demo titles + descriptions and nav labels; every string now flows through the showcase message namespace (en / zh-CN / zh-TW), matching every other section.
  • Showcase gained the missing demos — a GenerationStats metric row in the chat assistant message (tokens / throughput / latency, beside the model badge) and a standalone ContentImage (a self-contained local SVG scene, so the lazy-loaded, click-to-zoom frame always renders without an external fetch).
  • SegmentedBar docs page no longer 500s — its "Total and remainder" demo passed a live formatValue function inline from the (server-rendered) MDX, which React Server Components reject. Moved to a client demo component (SegmentedBarLegendDemo), matching the AreaChart pattern.
  • Select partsSelectLabel, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton (round out the Radix Select surface).
  • PopoverAnchor — position a Popover relative to a separate anchor element.
  • Badge structural variantssecondary, outline, and destructive (alias of error) alongside the existing semantic variants, for shadcn-style call sites.
  • @yuhuanowo/yunui/content (new subpath) — the content-rendering stack. Rich rendering for LLM/chat/doc content, extracted from the Agent (garvea) project (the most complete of our three apps) so all of them share one canonical implementation: MarkdownRenderer (GFM tables/task-lists, KaTeX math, Shiki code, Mermaid diagrams, GitHub-style callouts, heading anchors, lazy zoomable images), plus standalone CodeBlock/InlineCode (Shiki), CalloutBlock, MathRenderer/InlineMath/BlockMath, MermaidDiagram, ContentImage and ImageLightbox. Decoupled via props: urlTransform (rewrite image/link URLs) and onCodeEdit (open code in an editor). The heavy engines are optional peerDependencies (react-markdown, remark-gfm, remark-math, rehype-raw/-katex/-slug, katex, shiki, mermaid) and Shiki/Mermaid load on demand, so content.js itself stays ~36 KB. Ships @yuhuanowo/yunui/content.css; consumers also import katex/dist/katex.min.css.
  • @yuhuanowo/yunui/chat (new subpath) — the chat pattern. Presentational, slot-based building blocks for AI chat UIs (no data/model types, no i18n): ChatMessage (avatar + header/badges + body + footer + hover actions), ChatMessageList (smart stick-to-bottom scroller), ChatComposer (auto-growing textarea, Enter-to-send, send/stop, attachment + toolbar slots) and ChatHeader (backdrop-blur shell with slots). Pairs with @yuhuanowo/yunui/content. No new dependencies.
  • GenerationStats (chat) — a compact metric-pill row for an assistant message (tokens, throughput tok/s, latency, and any custom pills), sitting beside the model badge in a ChatMessage header. Presentational; the host formats and supplies the values.
  • AreaChart (primitive) — an interactive line/area chart for a time series: smooth bezier curve, gradient area fill, dashed grid, and a hover guide with a value tooltip. Container-width (ResizeObserver), pure SVG. The "full" chart counterpart to Sparkline. Extracted from Yunxin's analytics charts so both apps (and Yunshu) share one canonical chart.
  • Sparkline (primitive) — a tiny inline SVG line/area chart for a single number series (throughput, GPU utilization, latency), tone-colored and container-width. No chart library.
  • Gauge (primitive) — a circular 0–100 percentage ring with a centered value or custom label; the arc color follows a semantic tone.
  • SegmentedBar (primitive) — a proportional multi-segment bar toward a total (memory allocation, request mix) with an optional value legend. Complements MetricBar's single fill.
  • FileDropzone (primitive) — a drag-and-drop / click-to-browse upload target that reports files via onFiles; presentation + interaction only.
  • AudioPlayer (patterns) — a compact controls bar around an HTML5 <audio>: play/pause, seek, time, and an optional download button.
  • MediaGallery (patterns) — one canonical result surface for generation pages: a grid/list of media results (image · video · audio) with per-item status (pending/processing/completed/failed), a progress bar, signed-URL expiry detection, and hover download/delete/preview. Extracted so Yunxin's image/video/audio galleries and Yunshu's generation pages share one component.
  • Full docs (3 locales), showcase demos and prop tables for all of the above. Motivated by the Yunshu webui rewrite onto YunUI and the tri-repo unification.

[0.2.15] - 2026-06-27

Added

  • PageLayout (patterns) — the standard full-height page shell: a navbar slot, a flex-1 <main> offset (pt-28) to clear the fixed navbar, and a footer slot. Navbar/footer are passed as slots so the shell stays decoupled from their props; hideFooter, transparentBg and mainClassName cover the common variations. Extracted from Yunxin's hand-rolled shell (used by 8+ marketing pages).
  • Original OKLCH palette system — 24 palettes. The design-token scheme layer is now generated from an in-house OKLCH model (scripts/gen-tokens.mjs) with perceptually-even ramps; added teal, lime, amber, plum and fuchsia (19 → 24). The brand/accent/neutral roles can target any of them at runtime.
  • YUNUI_THEME_PRESETS — 12 curated multi-color themes. Named {brand, accent, neutral} combos (Aurora, Sunset, Forest, Ocean, Grape, Ember, Lagoon, Blossom, Royal, Citrus, Orchid, Mono), exported with YunUIThemePreset / YunUIThemePresetName. Apply with applyTheme(preset).
  • Theme-effect utilities (in the stylesheet, token-driven so they restyle with the active brand/accent): .bg-brand-gradient, .text-brand-gradient, .glow-brand, .glow-accent, .bg-brand-sheen.

Fixed

  • iOS Safari no longer zooms the page when focusing a field. Mobile Safari auto-zooms when a focused input/textarea/select has font-size < 16px (our fields use text-sm = 14px). Added a touch-only (hover:none + pointer:coarse) rule flooring form fields at 16px; desktop keeps the 14px look. Verified in WebKit and Chromium under iPhone emulation.
  • Escape closes the LanguageSwitcher / ThemeToggle dropdowns (cross-browser). They previously only closed on outside-click. Added Escape-to-close with focus return to the trigger — via a document-level listener, because WebKit/Safari on macOS doesn't focus a <button> on click, so a container onKeyDown would never receive the key (caught by testing in the WebKit engine). CustomSelect now focuses its trigger on mouse-open for the same reason, so its keyboard nav works in Safari too.
  • Combobox is keyboard-navigable. Added ArrowUp/Down/Home/End/Enter listbox navigation, role="listbox"/role="option"/aria-selected, and aria-activedescendant — previously mouse-only.
  • Accessible names + ARIA on the dropdowns. aria-label on the ModelSelect / CustomSelect / Combobox / SearchInput search fields (placeholder alone isn't a label); aria-expanded/aria-haspopup on the ThemeToggle trigger; the Navbar mobile-menu scrim is aria-hidden.
  • Navbar respects the iPhone safe area. The fixed top bar uses max(1.5rem, env(safe-area-inset-top)) so it clears the notch / Dynamic Island in standalone Safari (no change on desktop, where the inset is 0).
  • Horizontal scroll bars no longer scroll vertically on touch. overflow-x-auto with the default overflow-y: visible makes a browser compute overflow-y to auto too, so the ModelSelect provider-filter row (and the NavTabs / Tabs bars) could be dragged up/down a few px on a phone. Added explicit overflow-y-hidden overscroll-x-contain to those single-row scrollers.
  • Unbreakable text no longer widens its row/card. Added truncate to the LinkRow title, SessionItem detail, ModelCard developer label and the Navbar app name (with min-w-0 on the logo link), so long ids / device strings / brand names ellipsize instead of pushing the layout past the viewport.
  • Hand-rolled dropdown panels stay inside the viewport. ModelSelect, CustomSelect, Combobox, LanguageSwitcher and ThemeToggle positioned their floating panel with a plain absolute and no collision handling, so on narrow / mobile screens the panel (notably ModelSelect's fixed w-96) spilled off the right edge and could run past the bottom. New useAnchoredPosition hook measures each panel against the viewport and applies a horizontal marginLeft nudge (margin, so it never fights framer-motion / animate-in transforms) plus a maxHeight cap paired with an internal flex-1 min-h-0 scroll region. The Radix panels (Select, DropdownMenu, Popover) already flip/shift via Popper and are unchanged.

Changed

  • All dropdown / select panels share the LanguageSwitcher look. Unified every floating menu panel onto one chrome — rounded-2xl border border-border bg-background/60 backdrop-blur-2xl shadow-lg shadow-black/5 — across Select, CustomSelect, Combobox, DropdownMenu, Popover, ModelSelect and the Navbar mobile menu (they previously diverged: square rounded-xl, opaque bg-(--bg-elevated) / bg-popover, weaker/absent blur, shadow-md/shadow-2xl). The selectable rows in Select/CustomSelect/Combobox now use the shared .dropdown-item class — the same left accent bar on hover/selection as the language picker, replacing per-component check-mark / tinted-background styles. .dropdown-item was extended to also drive Radix rows (keyboard data-highlighted, selected data-state=checked), so hand-rolled and Radix dropdowns render identically.
  • Palette values are now original. The scheme primitives are regenerated from the OKLCH model rather than the previously vendored ramps, so semantic-token consumers may see subtle color shifts. The generator and all values are first-party.

[0.2.14] - 2026-06-27

Changed

  • Softer corner radii across the core surfaces. Bumped the global component classes a notch rounder — .btn 12→14, .btn-sm 9→11, .btn-lg 14→16, .card/.glass-card 16→20, .stat-card/.gradient-card/.glass-card-enhanced 20→24, .input/.code-block 12→14, .badge 6→8, .nav-item/.nav-tab/ .dropdown-item 10→12. Literal px only — the Tailwind --radius-* namespace is deliberately untouched (defining it there hijacks every rounded-* utility).

Fixed

  • True-black (OLED) surfaces are no longer invisible. --color-muted/ --color-card map to --bg-elevated/--bg-card, which true-black set to pure #000 — so every bg-muted/bg-card fill (hover states, tiles, switch tracks, sticky headers, cards) vanished into the black page. Lifted elevated→#141414 and card→#0a0a0a (page stays #000), so fills are visible while borders/rings still delineate.
  • Switch off-state is visible in true-black. The unchecked track was bg-transparent with only a faint --border-strong outline (≈invisible on #000); it now carries a bg-muted fill so the track reads in every theme.
  • White ink on colored fills stays white in true-black. Added .text-pure-white (immune to the true-black .text-white#d4d4d8 body-text dim) and used it for the NotificationBell count badge and AvatarUploader overlay icons, which were dropping to a low-contrast grey on their red / black fills.
  • ConnectedAccountRow provider badge sits on the avatar edge. It was pinned to the bounding-box corner (-bottom-1 -right-1), floating off the circular avatar; now bottom-0 right-0 so it overlaps the ring cleanly.
  • ModelSelect hover stutter (esp. Safari). Each sticky group header added its own backdrop-blur on top of the panel's — 5 stacked backdrop-filter layers, which makes hover/scroll repaint janky. The header now uses a solid bg-muted fill (1 filter layer total).
  • ModelSelect provider group headers no longer render as a bright white box. The sticky header laid its own bg-popover/95 fill on top of the already translucent bg-popover/90 panel, so the band stacked to a brighter/whiter surface — a hard square seam, very visible over a tinted page. It's now a rounded rounded-xl bar with a muted (not popover-white) fill that echoes the model rows — a deliberate on-brand label that still occludes scrolling rows, instead of a stray white box.
  • ModelSelect keyboard-highlight ring no longer sits on the top row by default. The arrow-key highlight now starts at "no selection" (-1) instead of index 0, so the white focus ring appears only once the user actually presses Up/Down — and clears on mouse move so it never competes with hover. Enter with nothing highlighted still picks the top result (classic combobox).
  • Checkbox no longer crashes when used uncontrolled. checked now defaults to false and onCheckedChange is optional (guarded), so a display-only checkbox (no handler) renders instead of throwing on click.
  • ModelManagerCard no longer stretches to fit a long model name. The name now carries min-w-0 break-words, so an unbreakable long name wraps inside the card instead of widening the whole column.
  • .badge is now an atomic unit. Added white-space: nowrap + flex-shrink: 0 so a badge keeps its icon and label on one line and wraps as a whole in a narrow flex column, instead of being squeezed until a CJK label breaks per-character.
  • LanguageSwitcher pill collapses to icon-only when space is tight. The label is hidden sm:inline (icon + aria-label still convey the control), so the pill degrades to a round globe button on narrow widths instead of overflowing.

Added

  • SessionItem (patterns) — a row in an active-sessions / signed-in-devices list: device glyph, name with current / inactive badges, a browser · OS line, an IP + last-seen footer, and a revoke button. Host maps device→icon, formats the time, owns revoke.
  • MetricBar (patterns) — a labelled row with a thin proportion bar for "top N" breakdowns (spend-by-provider, usage-by-category, storage-by-bucket): icon (or color dot) · label · right-aligned value · colored bar.
  • InlineStatus (primitive) — a compact inline async-job status: a (spinning) icon plus a host-supplied label, or a percentage while running (pending / processing / completed / failed). Distinct from StatusIndicator (a presence dot) and StatusBadge (an approval pill).
  • FeatureLockedState (patterns) — the centered "feature unavailable" screen (icon medallion · title · description · optional dashed restricted-note card), for rendering as a feature-flag fallback. Presentation only; the host gates.
  • Banner (patterns) — a tinted, horizontal banner row (info / warning / critical / success / neutral) with an icon, title, inline description, meta, actions and an optional dismiss. One component for announcement, release and verification-style banners (distinct from the stacked Alert primitive).
  • NotificationBell / NotificationItem / NotificationPanel (patterns) — presentational notification-center pieces: a bell trigger with an unread-count badge, a notification row (type-glyph slot · title · body · time, optional adapter-Link + hover dismiss), and the dropdown chrome (header, scrollable list with loading / empty states, footer slot). The host owns fetch / auth / polling / mark-as-read and the open state; copy is passed in.
  • SimplePagination (patterns) — a prev / page-indicator / next pager for cursor- or has-more-style lists where the total page count isn't known (so BlogPagination's numbered pages don't apply). Same ghost-button styling; hasNext drives the next button, labels.page renders the centre indicator.
  • ModelManagerCard (ai) — a dense admin model row rendered as a card: header (select · icon · name + id chips · row actions), a 2-column labelled spec grid (provider, developer, type, status, context or resolution, max output, price in/out), and capability badges — every admin-table column present, top-to-bottom, so model management reads on any width instead of a wide scrolling table. All values are slots. Long, unbreakable names wrap inside the card.
  • SettingRow (patterns) — one labelled row in a settings / preferences list: title · optional description · trailing control slot (a Switch, Select, button…). Stack several in a card; a bottom border divides them and the last one drops it. Presentation only — the host owns the control and its state.
  • LinkRow (patterns) — a tappable row that links somewhere: leading icon · title + description · trailing chevron. For support links, settings navigation, "manage X" entries. external opens a new-tab anchor; otherwise it routes through the adapter Link.
  • ConnectedAccountRow (patterns) — a row in a connected-accounts / integrations list: a provider avatar (image, or a glyph in a ring) with a small provider badge overlay · name + optional sub-name · detail line · connected-time footer · unlink button. Host owns the unlink.
  • AvatarUploader (patterns) — a clickable avatar that opens a file picker: shows the image or an initials fallback, a camera overlay on hover, and a spinner while uploading. onSelectFile hands back the chosen File; the host uploads.

[0.2.13] - 2026-06-26

Added

  • ModelSelect (ai) — a generic, domain-agnostic searchable model picker: provider grouping, provider + capability filters, a pinned section, and a glassy dropdown. Consumers map their model type to ModelSelectOption (icon/badges/ detail/meta are slots) and own pinning + filters via props — no app types, pricing or i18n leak into the design system. Selection uses YunUI's left-bar signature (a full bar when selected, a faint bar that slides in on hover, like the Sidebar). New exports: ModelSelect, ModelSelectOption, ModelSelectFilter, ModelSelectLabels.
  • CapabilityIcon (ai) — the glyph-only capability indicator (the colored icon shown inline after a model name), built on the same shared capability config CapabilitySelector uses. Plus isKnownCapability. (The labelled pill CapabilityBadge already lived in patterns.) The capability config also gained dark: text variants, improving CapabilitySelector dark mode.
  • StatCard compact prop (patterns) — the lighter card p-4 tile for dense stat grids (keeps the dark-mode tone colors inline versions lacked).

Changed

  • Unified glassy overlay surfacesDialog, Modal, DropdownMenu, Select and Sheet now share the navbar's glassy treatment (bg-…/90 backdrop-blur-xl) instead of flat opaque slabs. Sheet is a floating, fully-rounded drawer.
  • Mobile-responsive layoutsPageHeader / MediaPageHeader stack title and actions on small screens; Grid and CapabilitySelector ramp columns mobile-first; Navbar, Footer, CodeBlock, Pagination and Table were tightened for narrow viewports.

Fixed

  • <Table responsive> no longer overflows on mobile. The stacked card used a label-left / value-right row that pushed wide cell content (icons, badges, long names, action buttons) off the card and clipped it. Cells now stack label-above-value, full-width, so any content stays on-screen.
  • azure / aws / bedrock rendered GitHub's icon and the "GitHub Models" name — a stale "served-via-GitHub-Models" shortcut. They now resolve to their own icon and proper display name.
  • Pagination active-state and width on mobile (no stray overflow box on tap).

[0.2.12] - 2026-06-25

Changed

  • Brand icons now come from @lobehub/icons-static-avatar (MIT) — the avatar variants (logo on a brand-colored rounded tile) instead of bare logos, so every provider/model icon reads as a consistent app-style tile in both light and dark (the previous bare SVGs looked "naked" on cards). scripts/sync-icons.mjs now vendors the FULL avatar set (309 brands) into icons/providers/ and generates PROVIDER_ICON_SLUGS, so any brand Yunxin references (e.g. opencode, which was missing) resolves without a hand-maintained map entry. Mono-only / non-lobe brands keep their existing raster.

[0.2.11] - 2026-06-25

Fixed

  • Provider/model icons resolved to the wrong extension after the 0.2.10 lobe sync. The sync rewrote 43+ rasters to .svg but providerIconMap/modelIconMap still named the old .png/.webp, so those icons 404'd and fell back to initials. scripts/sync-icons.mjs now also rewrites the filename literals in the icon maps to match the files on disk (75 entries corrected).

[0.2.10] - 2026-06-25

Added

  • Icon sync from @lobehub/icons-static-svg (MIT). scripts/sync-icons.mjs pulls fresh brand-COLOR SVGs for providers/models/apps into icons/, so the set stops drifting — re-run pnpm icons:sync to refresh or add brands (alias map handles name differences; mono-only brands keep their raster to avoid the currentColor-in-dark-mode issue). 43 providers upgraded to vector this pass.
  • <Table responsive> — dense, many-column tables stack each row into a labelled card below the md breakpoint instead of forcing a horizontal scroll, so they stay readable on narrow screens. Pair with <TableCell label="…"> to label each value. (Wide screens render the normal table; pages should avoid forcing whitespace-nowrap/min-width on every column so it can fit the viewport.)
  • Checkbox now accepts checked="indeterminate" (renders a dash) for partial select-all states.

Changed

  • Sheet is now a general slide-in drawer, shown on all screen sizes by default — it no longer hard-codes lg:hidden. Pass mobileOnly for the old hide-on-lg+ behavior (a drawer that only exists on mobile). Fixes the Sheet appearing to "not open" on desktop.

Fixed

  • Safari rendering. PasswordInput's reveal toggle is now a flex child (it was absolutely positioned, which Safari pushed outside the field), and NumberInput hides the native number spinner via plain CSS in styles/yunui.css — the Tailwind [&::-webkit-*] arbitrary variants were ignored by Safari, so it showed a duplicate spinner next to the −/+ steppers.

0.2.9 - 2026-06-24

Added

  • SearchInput gains a size prop ("sm" | "md", default md). sm is a compact 32px-tall variant for toolbars and dropdown filters (e.g. the model picker), keeping the same icon size so it reads as a smaller version, not a different control.
  • Combobox gains creatableFilter (only offer the "create new" row for inputs passing a test) and creatableIcon (custom glyph for that row) — so consumers like Yunxin's icon-URL combobox can single-source from YunUI instead of forking the component.

0.2.8 - 2026-06-24

Fixed

  • .card-col no longer sets height: 100%. That explicit height overrode the grid's align-items: stretch and, when the grid's own height was indefinite (e.g. a centered flex container), collapsed each card to its content height — so the footer-pinning silently failed. Stretch alone is the robust path; footers now align in any grid context.

0.2.7 - 2026-06-24

Added

  • SearchInput — the canonical search field: a leading magnifier with the right padding (typed text never sits under the icon) plus an optional clear button. Uses type="text" + role="searchbox" so there's no duplicate native ✕, restores focus after clearing, and onChange returns the string directly. Replaces hand-rolled <input> + absolute icon search boxes.
  • More common primitives: Separator (h/v divider), Alert (info/success/ warning/error callout with icon + title), Tag (small, optionally removable label), and AvatarGroup (overlapping avatars with a "+N" overflow chip).
  • Status & docs primitives: StatusIndicator (colored status dot + label, optional pulse), InlineCode (inline code span), and Steps (vertical progress stepper with done/active/upcoming states).
  • .card-col + .card-footer CSS classes: lay a .card out as a column with its footer pinned to the bottom, so equal-height grid cards line their footers up regardless of body content. Single-sources the card-footer-alignment fix here (instead of per-consumer inline flex tweaks that keep regressing).

0.2.6 - 2026-06-24

Added

  • New primitives closing part of the breadth gap: PasswordInput (masked field + show/hide reveal toggle), NumberInput (−/+ steppers with min/max clamping), and Kbd (inline keyboard-key display). All additive, same styling/error API as Input.
  • useFocusTrap(ref, enabled) hook. Modal (and ConfirmModal/DeleteConfirmModal/ RegenerateConfirmModal, which build on it) now trap focus — focus moves into the dialog on open, Tab/Shift+Tab cycle inside it, and focus returns to the opener on close. Closes the a11y gap flagged in 0.2.5.

Fixed

  • Showcase "Code" tabs now match the rendered preview (Button shows all 7 variants; Radix Select shows 3 items) and use the real @yuhuanowo/yunui import specifier (the in-repo yunui alias only resolves inside this repo).

0.2.5 - 2026-06-24

Added

  • Broad opt-in brand theming. With data-accent-source="brand" (+ data-brand) on <html> — or applyTheme({ accentSource: "brand", brand: "blue" }) — the brand color now flows across the whole UI, not just a couple of buttons: primary/accent buttons, the --color-primary active/selected states (Switch, Checkbox, RadioGroup, selected combobox/select rows…), Slider range + thumb, Progress fill, focus rings (--color-ring), and the bg-accent/text-accent utilities. Body text stays on --color-foreground, so readability is untouched. Default monochrome is byte-identical (the markers/overrides do nothing without the attribute; verified visually with a violet brand across the showcase). Also covers active navigation indicators (sidebar nav-item bar, NavTabs underline, Navbar active-link underline + Sign-up CTA) and the Radix Tabs active pill (translucent brand tint). Brand buttons use a translucent brand tint (keeps YunUI's glassy feel, not heavy solid blocks) and render correctly in dark mode (the brand button overrides are placed after the .dark .btn-* rules so they win in both modes).
  • A live "Live brand theming" switcher in the showcase (Mono/Brand + palette swatches) that dogfoods useYunUITheme and re-themes the whole page on click.

Changed

  • "Which one do I use" guidance on the overlapping component families, via JSDoc (visible in IDE + props tables), so picking one no longer feels like a trap: 3 dialogs (Dialog = accessible default / Modal = styled prop-driven / ConfirmModal* = confirmations) and 4 selects (Select / CustomSelect / Combobox / SegmentedSelect), plus Tabs vs NavTabs. Non-breaking.
  • Corrected Modal's docstring to state it does not trap focus (use Dialog when keyboard focus containment matters) — was previously implied "accessible".
  • Demarcated @yuhuanowo/yunui/ai as the app-domain layer (AI-gateway components), not general-purpose primitives — so the core surface reads as a clean design system (core → ., patterns → /patterns, app domain → /ai).

Internal

  • CI: bumped actions/checkout + actions/setup-node to v5 (Node 24).

0.2.4 - 2026-06-24

Added

  • Opt-in brand accent — bridge the legacy monochrome accent to the runtime token system. Set data-accent-source="brand" (+ data-brand) on <html>, or applyTheme({ accentSource: "brand", brand: "blue" }), to make accent-driven components (.btn-accent, bg-accent/text-accent, …) follow your brand color across every project. Purely additive: without the attribute the default monochrome look is byte-identical (verified by compiled CSS diff). New YunUIAccentSource type.

0.2.3 - 2026-06-24

Added

  • Bundled icon set expanded to the full shared asset library sourced from Yunxin: providers (88), models (171), apps (55), paintings (7), search (5) — 326 files. YunUI is now the single source of truth for these icons; any project can pull them from the package or via jsDelivr (…/@yuhuanowo/[email protected]/icons/<category>/<name>), instead of keeping its own copy.

0.2.2 - 2026-06-24

Changed

  • AI icons are now bundled and served via CDN by default. The package ships the provider/model icon set under icons/, and iconBasePath now defaults to jsDelivr (https://cdn.jsdelivr.net/npm/@yuhuanowo/[email protected]/icons) — so ProviderIcon / ModelIcon / ModelCard render with zero setup.
    • To self-host and extend with your own icons, copy the package's icons/ into your app and set iconBasePath (e.g. "/icons").
    • Behavior change for self-hosting consumers that relied on the old /icons default (e.g. Yunxin): set iconBasePath: "/icons" to keep using your own copy. next/image users: allow cdn.jsdelivr.net or self-host.

0.2.1 - 2026-06-24

Added

  • Runtime design-token system (styles/tokens.css, generated by scripts/gen-tokens.mjs): a layered 3-tier system — 19 palettes × 12 steps → role map → semantic — switchable at runtime via data-brand / data-accent / data-neutral on <html>. Exposed as Tailwind utilities (bg-brand-solid-strong, text-accent-on-background-weak, …) and a JS API (applyTheme, useYunUITheme, YUNUI_PALETTES). Additive — the default look is unchanged.
  • Button variant="brand" — solid fill driven by the themeable brand token.
  • Footer is now also re-exported from @yuhuanowo/yunui/patterns.
  • Docs demos have a single source of truth: <ComponentPreview> code tabs are auto-derived from the rendered children (remark plugin), so the shown code can't drift from the live preview.

Fixed

  • Marquee rendered as static text — added the missing --animate-marquee(-vertical) @theme tokens + keyframes.
  • Accessibility/correctness: Input/Textarea set aria-invalid / aria-describedby + disabled styling; IconButton sets aria-label; SegmentedSelect buttons are type="button" + aria-pressed; Switch/Checkbox gained focus-visible rings.
  • Crash fix: AI icon helpers (ProviderIcon / ModelIcon / getIconPath / getProviderName) no longer throw on an undefined provider id; tightened the fuzzy id matcher (no more "ai" → OpenAI false positives); ProviderIcon falls back gracefully when an icon asset 404s.
  • Honest README: cross-project sync lands on the next pnpm up, not "instantly".

Developer experience

  • pnpm dev now runs the lib watch + site dev together (concurrently); added pnpm tokens to regenerate the token CSS.

0.2.0 - 2026-06-23

Added

  • Layout primitives (the big layout-primitive gap): Flex, Grid, Column, Row, Stack — token-aware gap/padding/align/justify/columns via static class maps (Tailwind-compiler-safe).
  • Accordion + RadioGroup (Radix-backed, full keyboard a11y).
  • Data display: Table family (Table/Thead/Tbody/Tfoot/Tr/Th/Td + aliases), Breadcrumb family, controlled Pagination (ellipsis truncation, a11y labels).
  • Tests for all of the above (141 total, up from 101).

Fixed

  • license field corrected to Apache-2.0 to match the repo's LICENSE file. 0.1.4–0.1.8 wrongly declared MIT in package metadata; this release fixes it.
  • CodeDemo sample snippets use a generic api.example.com endpoint instead of a product-specific one.

0.1.8

Added

  • JSDoc on ~150 public props + component descriptions across primitives, patterns, and ai entries — surfaces in editor IntelliSense (ships in .d.ts).
  • Test suite expanded to 101 tests (from 21): interactive primitives (Switch, Checkbox, Slider, Tabs, Label), overlays/patterns (Modal, FAQ, Badge variants), and AI components (ModelCard, CapabilitySelector, IDBadge, ModelTypeIcon, Navbar, Footer). vitest setup stubs ResizeObserver.

Changed

  • Button variant aliases default/red documented as @deprecated (prefer primary/destructive); kept for backward compatibility.

0.1.7

Added

  • iconBasePath adapter option — configure where yunui/ai icon assets are served (default /icons; point it at /assets/icons, a CDN origin, …). Resolves the out-of-box broken-image issue for non-default hosting. Custom iconUrl props still pass through untouched.
  • publint packaging lint in CI + release.
  • 3 tests for ProviderIcon icon-base resolution (21 tests total).

Changed

  • README icon caveat now documents the configurable iconBasePath.

0.1.6

Added

  • CHANGELOG.md and a fuller README (install, Tailwind @source, adapters, exports, the AI icon-asset requirement, hooks, sync workflow).
  • Test suite: vitest + Testing Library; 18 tests covering core primitives, StatCard variants, and CustomSelect keyboard/a11y.
  • CI: ci.yml (typecheck + test + build + pack) and release.yml (tag v* → tokenless OIDC trusted publishing + provenance; no NPM_TOKEN).

Changed

  • CustomSelect is now an accessible combobox: ARIA roles (combobox/listbox/option + aria-expanded/selected/activedescendant) and full keyboard nav (↑/↓, Home/End, Enter, Escape, Tab) with highlight-into-view. Previously mouse-only.

Fixed

  • site/ workspace dependency aliased to @yuhuanowo/yunui (broke after the package was scoped).

0.1.5

Added

  • yunui/ai: ModelTypeIcon, ProviderIconImg (delegates to ProviderIcon), and buttonVariants (cva, for fumadocs-themed docs buttons).

Notes

  • _deferred/ai-search stays unexported — it is a full ai-sdk feature, not a reusable primitive, and would force @ai-sdk/react peer deps on the library.

0.1.4

Added

  • Canonical StatCard with tone colors + inline and valueFirst layout variants (covers the dashboard / insights / analytics stat-card styles from one component).
  • Built dist/ is tracked in git so the package resolves over a git dependency without an install-time build.

Changed

  • <Card> now renders the .card utility class (matches the app-wide .card usages instead of bespoke inline classes).
  • Package scoped to @yuhuanowo/yunui (the bare yunui name is blocked on npm).

0.1.0 – Initial extraction

  • Yunxin's design system extracted into a standalone, versioned package:
    • yunui — atomic primitives (Button…DropdownMenu).
    • yunui/css — tokens, @theme, global classes, keyframes (3 themes).
    • yunui/adaptersYunUIProvider/useYunUI (Link/Image/router/i18n injection).
    • yunui/patterns — landing/blog/docs + layout components.
    • yunui/ai — Yunxin AI-domain components (prop-driven).
  • tsup build → ESM + .d.ts, code-split shared adapter context, "use client" preserved, deps externalized at Yunxin's exact versions.