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 on0.2.xfor a long run — releases are almost always patch bumps. A0.3.0/1.0.0bump 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 thetailwindcss-animateplugin, 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 neighbouringduration-200emitted fine, which hid the failure. Fixed by importingtw-animate-css(the v4 successor, API-compatible, a plain stylesheet rather than a JS plugin) fromstyles/yunui.css. - Semantic colour helpers had no working variants.
.bg-error-soft,.text-error,.bg-accent-subtleand friends were hand-written rules inside@layer components, which Tailwind's variant machinery cannot see — sohover:bg-error-soft,hover:text-errorandhover:bg-accent-mutedemitted 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 missingtext-accent-solid,border-accent-solid,border-accent-subtleandborder-accent-muted, which consumers were already using against no definition. ShinyButton's sheen never moved.@keyframes shimmerexisted but no--animate-shimmerwas registered, soanimate-shimmerwas a dead class; and the sheen layer sat at-z-10under arelative(notisolate) parent, so it painted behind the button's own opaque fill. Both fixed.CTASectionandMarketingHerogainedisolate, so their-z-10radial washes cannot escape the component and vanish behind an opaque ancestor.- Semantic tones no longer use the raw Tailwind palette.
BannerandStatCardwere built onblue/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.StatCardgains 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:hoverthat every rawclassName="card"surface gets; it now appliescard-interactive.- Modals sit on the overlay shadow ramp.
Modal,ConfirmModalandConfirmCloseDialogusedshadow-2xl— a far heavier drop than anything else in the system — andModalusedbg-background/95where every other overlay usesbg-popover/85. All three now use the standardshadow-lg shadow-black/5recipe. - Bare
borderno longer renders ascurrentColorinSegmentedSelect,ModelSelectandFeatureLockedState(they now setborder-border), so the border stops shifting hue with the text colour. - Misc token cleanup: the offline status dot (
zinc-400→muted-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--errortoken).
Fixed
BentoCardhover 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
useAnchoredPositionno 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 onerequestAnimationFramemeasurement instead of running agetBoundingClientRectreflow per scroll event, and the listener ispassive. Resize and theResizeObserverstay synchronous so content changes still remeasure promptly.Sidebarpersists its scroll position at most once per frame. It was writingsessionStoragesynchronously on every scroll event; the write is now rAF-coalesced and the listener ispassive.
Changed
-
Label props follow one rule now, and it is written down. They had drifted into a shape nobody could predict:
SessionItemtook four separate*Labelprops,NotificationPanelthree, andPaginationandBlogPagination— the same control twice — disagreed on whether it waspreviousLabel/nextLabelorlabels: { previous, next }. The rule is now in CONTRIBUTING.md:labelfor the component's own name,ariaLabelfor a container whoselabelalready means something else,<x>Labelfor exactly one auxiliary string, and alabelsobject for two or more. One-string components are deliberately left alone —labels={{ back: "…" }}is worse thanbackLabel="…".Breaking, for four components:SessionItemcurrentLabel/inactiveLabel/runningLabel/revokeLabel→labels.{current,inactive,running,revoke};NotificationPanelunreadLabel/loadingLabel/emptyLabel→labels.{unread,loading,empty};PaginationpreviousLabel/nextLabel→labels.{previous,next};ChatComposersendLabel/stopLabel→labels.{send,stop}.
Fixed
Paginationannounced 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 takeslabels.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 ofsrc/was swept for the same shape.--text-tertiarywas 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-labelinside a tinted card failed AA. This only became visible once the.nav-sectionfailure above stopped masking it. Now #91919a: 4.77 on the tints / 5.67 on elevated / 6.37 on base, still clearly below--text-secondaryso 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-foregroundhad no bridge in light or dark either. Onlytrue-blackoverrode 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-tertiaryin 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-blackfumadocs bridge was losing the cascade. Fumadocs ships its LIGHT token defaults as:root:not(.dark)unlayered.true-blacksetsclass="true-black"with nodark, so that selector matches it — and unlayered CSS beats every@layerregardless of specificity, so the.true-blackbridge in@layer componentsnever applied. Docs body text rendered at fumadocs' light#525252on 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-blackis (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 intrue-blackfor the first time — seesite/visual/axe-theme.mjs. - Code blocks rendered the LIGHT syntax theme in the
true-blacktheme — 1.35:1. Fumadocs emits--shiki-light/--shiki-darkper token and flips them with.dark .shiki … code span. next-themes maps one theme to one class, sotrue-blacksetsclass="true-black"with nodarkbeside it and that switch never fired: GitHub's light palette on #0a0a0a. A.true-blackcounterpart existed but sat in@layer components, where fumadocs' own light rule in@layer utilitiesbeat it — later layer wins, specificity is not consulted. It is unlayered now (a marked section at the end ofyunui.css), and covers both DOM shapes fumadocs emits (.shikion the wrapping figure, and on the<code>itself). Measured worst token after the fix: 7.45:1. CodeBlockline numbers were invisible when you hovered to read them.--text-muted(2.57:1 in dark before opacity) atgroup-hover:opacity-50works out to roughly 1.4:1. They still only appear on hover, but now in--text-tertiaryat full opacity: 6.37:1 dark / 6.72:1 true-black / 5.28:1 light.--color-fd-cardwas bridged only intrue-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 thefd-*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-sectionfailed AA on every page that has a sidebar. The uppercase 10px section headings ("PLAYGROUND", "GENERATE", …) used--text-muted, which in dark is#52525bon#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.useFocusTrapbailed onif (!container) returnwith deps ofenabledplus a ref object whose identity never changes — so once it bailed it never ran again, and portal dialogs rendernulluntil their SSR-safetymountedstate is set, so the container arrives a commit later.Sheet,ConfirmModalandConfirmCloseDialogpass&& mounted;Modalpassed bareisOpenand 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 ondocumentin 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.CodeDemowas hardcoded to one gateway. All three snippets baked inhttps://api.example.com/v1anddeepseek-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,labelsandclassNameare 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 fromsrc/(consumers install by git pin, so the committeddist/is the package and could drift silently);scripts/check-emitted-css.mjsasserts that every literalclassNameinsrc/produces a selector in the built stylesheet — the failure mode behindbg-error/10,hover:bg-error-softand the deadtailwindcss-animateclasses, all of which look perfectly ordinary in review — and also fails onfd-*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
ModelSelectgainsrenderHeader+filterResetKey—renderHeaderrenders a caller-owned toolbar pinned to the TOP of the open panel, always visible even when the result list is empty (unlikerenderFooter, 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.filterResetKeyimperatively 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
MermaidDiagramrestores its documented.mermaid-containerDOM 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.CodeBlockno 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 keepsMarkdownRendereron 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/60frosted panel, so content behind them showed through and read as if it were still clickable. They now usebg-popover/85(still frosted viabackdrop-blur, but opaque enough to occlude what's underneath) and the correct--popoversurface token. - Button labels never wrap — the
.btnbase now setswhite-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
TableHead—TableHeadgains optional, backward-compatible sort props:onSortmakes the header a sort control (renders its content as a<button>with an asc/desc/unsorted chevron and setsaria-sorton the<th>),sortDirection("asc" | "desc" | false) is the column's current state, andalign("left" | "right" | "center") aligns both the content and the control (use"right"for numeric columns). OmitonSortand it behaves exactly as before. Fills the data-table column-sorting gap without a separate component.TableHeadis also nowwhitespace-nowrapso 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.cardlanguage. Unlike theAgentStepspanel, the answer is atextblock among the tool/reasoning blocks in conversation order (no result-on-top / steps-below split); tool rows expand into$-prefixed monospace output, failures use semanticred-500/5tints, and anapprovalblock renders an amber allow/deny gate. Prop-driven and copy-free — the consumer maps records ontoAgentTimelineBlock, 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 viarole="status"/aria-live). ComplementsAgentTimeline: 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 stayaria-hidden.activepauses the sweep without a layout shift.ChatAttachment(@yuhuanowo/yunui/chat) — a compact attachment surface shared by the composer and sent messages: name/meta, an optionalprevieworicon, a determinate uploadprogressbar,status(idle/loading/error) affordances and anactionsslot.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 viaonOpen.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/backToToptoggle each half;thresholdandlabels.backToTopare configurable. One rAF-coalesced scroll listener; the ring animates viastroke-dashoffset.SettingsShell(@yuhuanowo/yunui/patterns) — the canonical settings layout: grouped desktop sidebar navigation, a compact mobileSelect, and one scroll-safe content lane; works in a dialog or full page. Controlled viavalue/onValueChange, sections fromgroups(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 inSidebar/SettingsShell; a presentational<span>driven byactive/runningdata-states. Extracted so bespoke navigation surfaces can match the system.CustomSelectserver-backed search + infinite scroll —onSearch(debounced bysearchDebounceMs, default 250) drives results from a backend, paired withloading;onLoadMore+hasMoreappend pages as the list nears its end. Both optional and independent of the existing client-sidesearchablefiltering.Comboboxgainsclearable(defaulttrue) — a clear (×) action for the current value; setclearable={false}when the field must always hold a value.useAnchoredPositionflips 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 sanitized —
MermaidDiagramrenders with a strictsecurityLeveland KaTeX withtrust: false, so diagram/math content coming from model or user input can't inject scripts or navigate via crafted labels (XSS hardening). Tooltipcontent escapes clipping ancestors — tooltips now portal out, so they're no longer cut off by anoverflow: hidden/clipparent.
Removed
AgentSteps(@yuhuanowo/yunui/ai) — the earlier Codex-style execution panel (result-on-top / steps-below split) is removed, superseded byAgentTimeline, which renders the same agent-turn data as an inline ordered sequence of typed blocks. No consumer shipped againstAgentSteps.
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 discoveruseDismissOnOutside,applyThemeorgetIconPathshort of readingdist. Three new pages, in all three locales: Hooks, Theming and Utilities. useAnchoredPositionanduseScrollableTabStopare 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) andProviderIconImg(Provider Icon). - The fourteen compound sub-exports are documented.
CardHeader/CardTitle/CardDescription/CardContent/CardFooter,SelectLabel/SelectSeparator/SelectScrollUpButton/SelectScrollDownButton,DropdownMenuGroup/DropdownMenuPortaland thefadeIn/staggerContainer/staggerItemvariant 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 barearia-label="…"literals remain insrc/.
Fixed
- The syntax theme failed AA in both modes.
github-light's constant colour (#e36209) measures 3.49:1 andgithub-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-defaultvariants GitHub itself moved to, which have no failing text token in either mode and are visually near-identical. - A YunUI
Checkboxcould not be given an accessible name at all. It renders a<button role="checkbox">, so<label>— wrapping orhtmlFor— does not name it; and the component dropped every prop it did not explicitly list, soaria-labelwas 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,MetricBarandChatAttachment's upload bar (role="progressbar"), andSlider'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 alabelnow (MetricBar reuses its visible one). TextShimmerannounced nothing. It putaria-labelon a role-less<span>, which ARIA prohibits and browsers ignore, while both paint layers werearia-hidden— so "Thinking…" was invisible to a screen reader. It renders a realsr-onlycopy now. Same for the markdown task-list checkbox, which was an unlabelled readonly control and is now correctly hidden.SegmentedBarandSparklineclaimedrole="img"with no name, telling a screen reader there is a picture and nothing about it. The role is now conditional on alabel.- Pagination's Previous/Next had no name on a phone. The word is
hidden sm:inline, which left a bare chevron belowsm— in bothBlogPaginationandSimplePagination. - Tinted badges failed AA. The capability pills,
FellowBadge,SourceBadgeand the capability selector used-600ink 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-700now; dark keeps-400. Meaningful glyphs moved-500→-600for SC 1.4.11's 3:1..badge-infoandStatCard's toned labels likewise. - Nested frosted glass never worked, and the code blamed the wrong thing. The
mobile menu inside
Navbarcarriedbg-popover/95 backdrop-blur-2xl, but the bar it is nested in has its ownbackdrop-filter— which makes that bar a backdrop root, so a nestedbackdrop-filtersamples an empty backdrop and renders nothing. Proven by measurement: forcing the panel's backdrop-filter tononeproduced 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. TheNavbarcomment that attributed this totransformhas 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 nowtext-base md:text-sm, matching the earlier fix to Input/Textarea/etc. LLMCopyButton,ViewOptionsandbuttonVariantsshipped unstyled. They were written againstfd-*utilities, but the--color-fd-*variables are declared only inside YunUI's true-black scope, never in@theme— so Tailwind generates nobg-fd-primary/hover:bg-fd-accent/ring-fd-ringat 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. NewuseScrollableTabStopmeasures actual overflow, so tables that fit gain no dead tab stop, andscrollLabelnames 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
#. /changeloghad two<main>landmarks — the marketing shell already wraps every page in one.
Added
Navbargained 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) andclassName.mobileMenuFooterdeliberately 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.CommandPaletterows can be real links. Give an item anhrefand the row renders as an anchor through the adapter'sLinkinstead 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 afooterslot (result counts / keyboard legend), a spinner in the input row whileloadingkeeps the previous results on screen, and properrole="listbox"/role="option"/aria-activedescendantwiring 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 formousedownonly — 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.PageLayoutnow putsid="main-content"on its<main>(overridable viamainId). Yunxin's root layout ships a "Skip to main content" link on every page, but#main-contentonly 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 throughPageLayout, so this fixes them without touching Yunxin.- Docs for the nine components that had none.
Kbd,Steps,Separator,AvatarGroup,MotionDiv/MotionSpan,NumberInput,PasswordInputandSearchInputwere 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-presenceand@radix-ui/react-switch. Neither is referenced anywhere in the source or the build —Switchis hand-rolled on a plain<button>— so they were pure install weight for every consumer. AuthShellis 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 flatp-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 theerror,iconandcenteredslots those screens actually use.- The last hardcoded English strings are now overridable. The library is meant
to carry no copy at all, but
DialogContentandSheetshipped a literalaria-label="Close",Comboboxshipped"Clear"/"Toggle options", andLLMCopyButton/ViewOptionsshipped six visible labels. All are now props (closeLabel,labels), with the English text kept only as a default so the components stay usable untranslated. ModelSelectselection bar inset refined to 5px (fromleft-1.5/6px), set as an inline style since Tailwind's1.25step isn't core and could be dropped by a consumer's JIT scan.AgentTimelinenow speaks YunUI's semantic status vocabulary — the error / success / warning affordances moved off raw Tailwindred-*/emerald-*/amber-*onto the--error/--success/--warningtokens, so they track the active theme (light / dark / brand) instead of drifting from the system's palette../package.jsonis 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,
shiki4,framer-motion12.43,lucide-react1.28 and more. No API changes;typescriptintentionally held at 6.x (7.x isn't yet compatible with thetsup/rollup-plugin-dtsbuild toolchain).
[0.2.16] - 2026-07-02
Fixed
ModelSelectselection bar sat flush against the row edge. The selected / hover accent bar wasleft-0; inset toleft-1.5so it no longer crowds the panel edge next to the model icon.ChatMessageListauto-scroll hijacked the whole document. Its stick-to-bottom usedendRef.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.MediaGallerygrid 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 sameImageLightboxthatContentImageuses (zoom / rotate / download / keyboard); a hostonPreviewstill overrides it.ModelManagerCardno longer looks top-heavy when it has actions but no row-select control. The top control bar (min-h-7 mb-3) rendered wheneverselectSlotORactionsexisted, so a card with onlyactions(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 forselectSlot; without it the actions ride on the identity row (right-aligned, aligned with the name). Multi-select admin mode (withselectSlot) is unchanged.Sheetbody 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).ThemeTogglepill no longer shows a lopsided blank stretch in dark mode. The Sun stayed in-flow whilescale-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 fromw-16to the trigger's real footprint.TabsListgainedmax-w-full— as aninline-flexit sized to content, so a long tab strip dragged the page sideways on narrow screens instead of engaging its ownoverflow-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
mutedbridge.--color-muted-foregroundand the.text-labelutility mapped to--text-muted(#52525b in dark) — only 2.57:1, so every consumer label/caption/section-header that usedtext-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-mutedalready equals--text-tertiary). Fixes the app-wide "washed-out details" seen across consumer surfaces. Gauge/Slidertracks 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.Sliderthumb no longer clips at the track extremes. The Root gainedpx-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).TabsTriggericon + label spacing. Addedgap-1.5so 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/*andsrc/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/--accentand the.card/.badge/*-softclasses — instead of the shadcn@themealiases (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. CodeBlockrewritten to YunUI's original code-block design — the.cardcontainer, window-chrome traffic-light dots, a language.badge,--bg-elevatedheader /--bg-basebody and--accent-subtlehover buttons, matchingpatterns/CodeBlock(keeps Shiki highlighting). Was a divergent terminal-header design lifted from Agent.MarkdownRendererroot isnot-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.
CalloutBlockmaps 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.CodeBlockandMarkdownRendererdrop rawbg-[#f6f8fa]/bg-green-100/bg-yellow-200forbg-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 becomeCalloutBlock(which composesAlert). - Shiki code was double-spaced — block
.linespans separated by literal"\n"text nodes rendered an extra empty line underwhite-space: pre.content.csscollapses the inter-line newlines while each.linekeepspre. - 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) andcodeis 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, nottext-*; switched toaccent-(--accent). - Code blocks drew a nested "box-in-box" — a leaked host/prose
codestyle (border + radius + chip background) bled into Shiki's<code>.content.cssnow neutralizes it, so code sits flat on one clean surface. - Touch a11y — code-block copy/edit buttons and
ChatMessagehover actions wereopacity-0 group-hover(invisible/unreachable on touch); now visible on coarse pointers (opacity-100 md:opacity-0 md:group-hover:opacity-100). - iOS focus-zoom —
ChatComposer's textarea wastext-sm(<16px); nowtext-base md:text-smso mobile Safari doesn't zoom on focus. - Aligned content surfaces to YunUI's radius language (
rounded-lg→rounded-xlon 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 loop —
MermaidDiagram(stuck on "Rendering diagram…") andMathRenderer(CPU thrash) looped forever becauseuseContentT()and the default adapter'suseTreturned a fresh function on every render, which was in those components' effect deps.useT/useContentTnow return stable references, and the async effects no longer depend on the translator. Only surfaced under real rendering — caught by screenshot QA. ChatMessageheader collided badges with the timestamp on narrow widths — aflex items-centerheader meant wrapping badges (e.g. a model chip plus aGenerationStatstoken/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.ContentImagespun forever when aChatMessageListshared the page — the lazy-loadIntersectionObserverrooted itself at the first[data-scroll-container="true"]element (whichChatMessageListsets). If the image sat outside that list (the normal content + chat page), it never intersected, so thesrcwas 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
MermaidDiagramis hand-drawn (Excalidraw-style) for every diagram type. Uses Mermaid 11's nativelook: "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.MermaidDiagramclick-to-zoom — newenableZoom(defaulttrue) opens the diagram in a full-screen lightbox on a theme-matched panel (readable in light and dark).ImageLightboxnow accepts arbitrarychildren(not just an<img src>), so any inline SVG can reuse its zoom/rotate/close controls.- Localizable labels for the last hardcoded strings —
AreaChartnoDataLabel,AudioPlayerlabels(play/pause/seek/download aria), andChatComposersendLabel/stopLabel, so every user-facing string in the new components is host-overridable. ChatComposerallowSendEmpty— 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.Cardsub-components —CardHeader,CardTitle,CardDescription,CardContent,CardFooterfor composing structured cards (previouslyCardwas 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
showcasemessage namespace (en / zh-CN / zh-TW), matching every other section. - Showcase gained the missing demos — a
GenerationStatsmetric row in the chat assistant message (tokens / throughput / latency, beside the model badge) and a standaloneContentImage(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
formatValuefunction inline from the (server-rendered) MDX, which React Server Components reject. Moved to a client demo component (SegmentedBarLegendDemo), matching the AreaChart pattern. Selectparts —SelectLabel,SelectSeparator,SelectScrollUpButton,SelectScrollDownButton(round out the Radix Select surface).PopoverAnchor— position a Popover relative to a separate anchor element.Badgestructural variants —secondary,outline, anddestructive(alias oferror) 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 standaloneCodeBlock/InlineCode(Shiki),CalloutBlock,MathRenderer/InlineMath/BlockMath,MermaidDiagram,ContentImageandImageLightbox. Decoupled via props:urlTransform(rewrite image/link URLs) andonCodeEdit(open code in an editor). The heavy engines are optionalpeerDependencies(react-markdown,remark-gfm,remark-math,rehype-raw/-katex/-slug,katex,shiki,mermaid) and Shiki/Mermaid load on demand, socontent.jsitself stays ~36 KB. Ships@yuhuanowo/yunui/content.css; consumers also importkatex/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) andChatHeader(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 aChatMessageheader. 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 toSparkline. 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 semantictone.SegmentedBar(primitive) — a proportional multi-segment bar toward atotal(memory allocation, request mix) with an optional value legend. ComplementsMetricBar's single fill.FileDropzone(primitive) — a drag-and-drop / click-to-browse upload target that reports files viaonFiles; 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, aflex-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,transparentBgandmainClassNamecover 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; addedteal,lime,amber,plumandfuchsia(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 withYunUIThemePreset/YunUIThemePresetName. Apply withapplyTheme(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/ThemeToggledropdowns (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 containeronKeyDownwould never receive the key (caught by testing in the WebKit engine).CustomSelectnow focuses its trigger on mouse-open for the same reason, so its keyboard nav works in Safari too. Comboboxis keyboard-navigable. Added ArrowUp/Down/Home/End/Enter listbox navigation,role="listbox"/role="option"/aria-selected, andaria-activedescendant— previously mouse-only.- Accessible names + ARIA on the dropdowns.
aria-labelon theModelSelect/CustomSelect/Combobox/SearchInputsearch fields (placeholder alone isn't a label);aria-expanded/aria-haspopupon theThemeToggletrigger; the Navbar mobile-menu scrim isaria-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-autowith the defaultoverflow-y: visiblemakes a browser computeoverflow-ytoautotoo, so theModelSelectprovider-filter row (and theNavTabs/Tabsbars) could be dragged up/down a few px on a phone. Added explicitoverflow-y-hidden overscroll-x-containto those single-row scrollers. - Unbreakable text no longer widens its row/card. Added
truncateto theLinkRowtitle,SessionItemdetail,ModelCarddeveloper label and theNavbarapp name (withmin-w-0on 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,LanguageSwitcherandThemeTogglepositioned their floating panel with a plainabsoluteand no collision handling, so on narrow / mobile screens the panel (notablyModelSelect's fixedw-96) spilled off the right edge and could run past the bottom. NewuseAnchoredPositionhook measures each panel against the viewport and applies a horizontalmarginLeftnudge (margin, so it never fights framer-motion /animate-intransforms) plus amaxHeightcap paired with an internalflex-1 min-h-0scroll 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— acrossSelect,CustomSelect,Combobox,DropdownMenu,Popover,ModelSelectand theNavbarmobile menu (they previously diverged: squarerounded-xl, opaquebg-(--bg-elevated)/bg-popover, weaker/absent blur,shadow-md/shadow-2xl). The selectable rows inSelect/CustomSelect/Comboboxnow use the shared.dropdown-itemclass — the same left accent bar on hover/selection as the language picker, replacing per-component check-mark / tinted-background styles..dropdown-itemwas extended to also drive Radix rows (keyboarddata-highlighted, selecteddata-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 —
.btn12→14,.btn-sm9→11,.btn-lg14→16,.card/.glass-card16→20,.stat-card/.gradient-card/.glass-card-enhanced20→24,.input/.code-block12→14,.badge6→8,.nav-item/.nav-tab/.dropdown-item10→12. Literal px only — the Tailwind--radius-*namespace is deliberately untouched (defining it there hijacks everyrounded-*utility).
Fixed
- True-black (OLED) surfaces are no longer invisible.
--color-muted/--color-cardmap to--bg-elevated/--bg-card, which true-black set to pure#000— so everybg-muted/bg-cardfill (hover states, tiles, switch tracks, sticky headers, cards) vanished into the black page. Lifted elevated→#141414and card→#0a0a0a(page stays#000), so fills are visible while borders/rings still delineate. Switchoff-state is visible in true-black. The unchecked track wasbg-transparentwith only a faint--border-strongoutline (≈invisible on#000); it now carries abg-mutedfill 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→#d4d4d8body-text dim) and used it for theNotificationBellcount badge andAvatarUploaderoverlay icons, which were dropping to a low-contrast grey on their red / black fills. ConnectedAccountRowprovider badge sits on the avatar edge. It was pinned to the bounding-box corner (-bottom-1 -right-1), floating off the circular avatar; nowbottom-0 right-0so it overlaps the ring cleanly.ModelSelecthover stutter (esp. Safari). Each sticky group header added its ownbackdrop-bluron top of the panel's — 5 stackedbackdrop-filterlayers, which makes hover/scroll repaint janky. The header now uses a solidbg-mutedfill (1 filter layer total).ModelSelectprovider group headers no longer render as a bright white box. The sticky header laid its ownbg-popover/95fill on top of the already translucentbg-popover/90panel, so the band stacked to a brighter/whiter surface — a hard square seam, very visible over a tinted page. It's now a roundedrounded-xlbar 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.ModelSelectkeyboard-highlight ring no longer sits on the top row by default. The arrow-key highlight now starts at "no selection" (-1) instead of index0, 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.Enterwith nothing highlighted still picks the top result (classic combobox).Checkboxno longer crashes when used uncontrolled.checkednow defaults tofalseandonCheckedChangeis optional (guarded), so a display-only checkbox (no handler) renders instead of throwing on click.ModelManagerCardno longer stretches to fit a long model name. The name now carriesmin-w-0 break-words, so an unbreakable long name wraps inside the card instead of widening the whole column..badgeis now an atomic unit. Addedwhite-space: nowrap+flex-shrink: 0so 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.LanguageSwitcherpillcollapses to icon-only when space is tight. The label ishidden sm:inline(icon +aria-labelstill 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, abrowser · OSline, 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 fromStatusIndicator(a presence dot) andStatusBadge(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 stackedAlertprimitive).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 (soBlogPagination's numbered pages don't apply). Same ghost-button styling;hasNextdrives the next button,labels.pagerenders 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 (aSwitch,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.externalopens a new-tab anchor; otherwise it routes through the adapterLink.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.onSelectFilehands back the chosenFile; 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 toModelSelectOption(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 configCapabilitySelectoruses. PlusisKnownCapability. (The labelled pillCapabilityBadgealready lived inpatterns.) The capability config also gaineddark:text variants, improvingCapabilitySelectordark mode.StatCardcompactprop (patterns) — the lightercard p-4tile for dense stat grids (keeps the dark-mode tone colors inline versions lacked).
Changed
- Unified glassy overlay surfaces —
Dialog,Modal,DropdownMenu,SelectandSheetnow share the navbar's glassy treatment (bg-…/90 backdrop-blur-xl) instead of flat opaque slabs.Sheetis a floating, fully-rounded drawer. - Mobile-responsive layouts —
PageHeader/MediaPageHeaderstack title and actions on small screens;GridandCapabilitySelectorramp columns mobile-first;Navbar,Footer,CodeBlock,PaginationandTablewere 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/bedrockrendered 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.Paginationactive-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.mjsnow vendors the FULL avatar set (309 brands) intoicons/providers/and generatesPROVIDER_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
.svgbutproviderIconMap/modelIconMapstill named the old.png/.webp, so those icons 404'd and fell back to initials.scripts/sync-icons.mjsnow 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.mjspulls fresh brand-COLOR SVGs for providers/models/apps intoicons/, so the set stops drifting — re-runpnpm icons:syncto 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 themdbreakpoint 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 forcingwhitespace-nowrap/min-widthon every column so it can fit the viewport.)Checkboxnow acceptschecked="indeterminate"(renders a dash) for partial select-all states.
Changed
Sheetis now a general slide-in drawer, shown on all screen sizes by default — it no longer hard-codeslg:hidden. PassmobileOnlyfor 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), andNumberInputhides the native number spinner via plain CSS instyles/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
SearchInputgains asizeprop ("sm" | "md", defaultmd).smis 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.ComboboxgainscreatableFilter(only offer the "create new" row for inputs passing a test) andcreatableIcon(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-colno longer setsheight: 100%. That explicit height overrode the grid'salign-items: stretchand, 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. Usestype="text"+role="searchbox"so there's no duplicate native ✕, restores focus after clearing, andonChangereturns the string directly. Replaces hand-rolled<input> + absolute iconsearch boxes.- More common primitives:
Separator(h/v divider),Alert(info/success/ warning/error callout with icon + title),Tag(small, optionally removable label), andAvatarGroup(overlapping avatars with a "+N" overflow chip). - Status & docs primitives:
StatusIndicator(colored status dot + label, optional pulse),InlineCode(inline code span), andSteps(vertical progress stepper with done/active/upcoming states). .card-col+.card-footerCSS classes: lay a.cardout 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), andKbd(inline keyboard-key display). All additive, same styling/error API asInput. useFocusTrap(ref, enabled)hook.Modal(andConfirmModal/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/yunuiimport specifier (the in-repoyunuialias 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>— orapplyTheme({ accentSource: "brand", brand: "blue" })— the brand color now flows across the whole UI, not just a couple of buttons: primary/accent buttons, the--color-primaryactive/selected states (Switch, Checkbox, RadioGroup, selected combobox/select rows…), Slider range + thumb, Progress fill, focus rings (--color-ring), and thebg-accent/text-accentutilities. 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
useYunUIThemeand 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), plusTabsvsNavTabs. Non-breaking. - Corrected
Modal's docstring to state it does not trap focus (useDialogwhen keyboard focus containment matters) — was previously implied "accessible". - Demarcated
@yuhuanowo/yunui/aias 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-nodeto 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>, orapplyTheme({ 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). NewYunUIAccentSourcetype.
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/, andiconBasePathnow defaults to jsDelivr (https://cdn.jsdelivr.net/npm/@yuhuanowo/[email protected]/icons) — soProviderIcon/ModelIcon/ModelCardrender with zero setup.- To self-host and extend with your own icons, copy the package's
icons/into your app and seticonBasePath(e.g."/icons"). - Behavior change for self-hosting consumers that relied on the old
/iconsdefault (e.g. Yunxin): seticonBasePath: "/icons"to keep using your own copy.next/imageusers: allowcdn.jsdelivr.netor self-host.
- To self-host and extend with your own icons, copy the package's
0.2.1 - 2026-06-24
Added
- Runtime design-token system (
styles/tokens.css, generated byscripts/gen-tokens.mjs): a layered 3-tier system — 19 palettes × 12 steps → role map → semantic — switchable at runtime viadata-brand/data-accent/data-neutralon<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. Buttonvariant="brand"— solid fill driven by the themeable brand token.Footeris 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)@themetokens + keyframes. - Accessibility/correctness:
Input/Textareasetaria-invalid/aria-describedby+ disabled styling;IconButtonsetsaria-label;SegmentedSelectbuttons aretype="button"+aria-pressed;Switch/Checkboxgained 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);ProviderIconfalls back gracefully when an icon asset 404s. - Honest README: cross-project sync lands on the next
pnpm up, not "instantly".
Developer experience
pnpm devnow runs the lib watch + site dev together (concurrently); addedpnpm tokensto regenerate the token CSS.
0.2.0 - 2026-06-23
Added
- Layout primitives (the big layout-primitive gap):
Flex,Grid,Column,Row,Stack— token-awaregap/padding/align/justify/columnsvia static class maps (Tailwind-compiler-safe). Accordion+RadioGroup(Radix-backed, full keyboard a11y).- Data display:
Tablefamily (Table/Thead/Tbody/Tfoot/Tr/Th/Td + aliases),Breadcrumbfamily, controlledPagination(ellipsis truncation, a11y labels). - Tests for all of the above (141 total, up from 101).
Fixed
licensefield corrected toApache-2.0to match the repo's LICENSE file. 0.1.4–0.1.8 wrongly declaredMITin package metadata; this release fixes it.CodeDemosample snippets use a genericapi.example.comendpoint 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
Buttonvariant aliasesdefault/reddocumented as@deprecated(preferprimary/destructive); kept for backward compatibility.
0.1.7
Added
iconBasePathadapter option — configure whereyunui/aiicon 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. CustomiconUrlprops still pass through untouched.publintpackaging lint in CI + release.- 3 tests for
ProviderIconicon-base resolution (21 tests total).
Changed
- README icon caveat now documents the configurable
iconBasePath.
0.1.6
Added
CHANGELOG.mdand 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,
StatCardvariants, andCustomSelectkeyboard/a11y. - CI:
ci.yml(typecheck + test + build + pack) andrelease.yml(tagv*→ tokenless OIDC trusted publishing + provenance; no NPM_TOKEN).
Changed
CustomSelectis 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 toProviderIcon), andbuttonVariants(cva, for fumadocs-themed docs buttons).
Notes
_deferred/ai-searchstays unexported — it is a full ai-sdk feature, not a reusable primitive, and would force@ai-sdk/reactpeer deps on the library.
0.1.4
Added
- Canonical
StatCardwithtonecolors +inlineandvalueFirstlayout 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.cardutility class (matches the app-wide.cardusages instead of bespoke inline classes).- Package scoped to
@yuhuanowo/yunui(the bareyunuiname 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/adapters—YunUIProvider/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.