Change Log
What's new in @viliha/vui-ui: features, changes, and fixes, newest first. Rendered straight from the package changelog, so it always matches what actually shipped. Filter by change type, or browse by release.
Upgrading from 1.44 to 1.52
Upgrade, restart, change nothing: everything from 1.50 on is additive, and the behaviour changes before it all point the same way, which is to stop showing people something meaningless. One case needs code, and only if you render
BrandAsset yourself: add inline (demo) or onPick (a real upload). The step-by-step guide, including what changed on its own and what you can opt into, is at the top of the package changelog. For how to configure any of it, see Configuration.v1.66
1.66.1
Changed
- The docs and demo moved to `internal.viliha.com`. Every link in the README, the agent guide and the package metadata points there now. The old
vui.viliha.comhost is gone, so a link that still used it would 404.
1.66.0
Added
- Three MCP tools for building a website.
list_blocks,get_blockandcompose_pageread@viliha/vui-webwherever it resolves, a sibling package in a monorepo ornode_modulesin a consumer's project, and describe it from the file list the way the component tools always have. A new block appears in the answers because the file exists. - The decoration a marketing page needs, in
theme.css:.vui-aurorafor a brand wash behind a hero,.vui-grid-bgand.vui-dot-bgfor a faint texture that fades out downward,.vui-glowfor a spotlight,.vui-gradient-textfor a gradient headline,.vui-liftfor a card that answers the pointer,.vui-ring-gradientfor a hairline brand edge, and.vui-revealfor a section that fades in as it scrolls into view. - `@viliha/vui-web` 0.2.0 ships the blocks these tools describe:
Marquee,CodeBlock,RatingBlock,PricingCalculator,UsagePricingTable, and agradienthero with breadcrumb and search slots. See its own changelog.
v1.65
1.65.0
Added
- `AvatarImage`. The avatar could only ever show initials: the package exported
AvatarandAvatarFallbackand nothing to put a picture in. The new export sits over the fallback and removes itself if the image fails to load, so a broken URL shows initials rather than an empty box, and a newsrcgets a fresh attempt. - `BrandAsset` gained `placeholder` and `fit`. The empty box said "None", which is right for a logo and wrong for a person, and the image was always
object-contain, which letterboxes a photo in a square. An avatar now passes initials andfit="cover"; a logo keeps both defaults.
v1.64
1.64.0
Fixed
- Register business had no menu, header or footer. It was a standalone route with its own cut-down layout, so reaching it from the organization switcher dropped you out of the app: no sidebar, no top bar, no footer, and no way back except the browser. It now lives inside the app shell like every other screen, with the standard page frame and a breadcrumb trail. The scaffolded template gets the same fix, since it is generated from this app.
- The help button in the top bar did nothing. It was a
<button>with no handler, so the question-mark icon was decoration. It opens the support screen now. The documentation icon beside it already worked; both, and the settings gear, gained a hover title so the cluster behaves consistently.
Added
- Right-to-left support, and layout as a preference.
theme.cssgains the appearance tokens behinddata-sidebar,data-densityanddir, and the reference app gains the picker that drives them. Most of RTL is free because the shell is flex-based and flex follows the reading direction; what was not free were the physical spacing utilities (ml-,pr-,right-), which are now logical (ms-,pe-,end-) throughout the app shell. Use the logical ones in new code and RTL keeps working. - Laravel and plain HTML are documented paths now, at
/docs/laraveland/docs/html. Neither gets a component package, and both pages say so. What they get is the theme, the Vite and Tailwind wiring, and copy-paste markup: Blade components with@propsand$attributes->merge()for Laravel, and a<link>tag plus snippets for a hand-written page. The snippets are generated from the same class strings the React and Vue components render, so a docs example cannot drift from the library. - Every default is documented as swappable, at `/docs/swapping`. Which icon set, which CSS engine, which font, which chart library, how much motion. The page says what each swap costs and, more usefully, what is *not* swappable: the components are Tailwind class strings, so a Bootstrap component set is not a drop-in and we do not pretend otherwise. Tokens are plain CSS variables, so those do bridge to any framework.
- Motion and the icon chip became tokens, so both can be retuned or turned off without forking the stylesheet. See the theme package changelog.
Changed
- The stacking-order test follows the class strings.
z-layers.test.tsreads source text, so moving a class intoclass-variants.tswould have left it checking a file that no longer mentionsz-[200]orbg-popoverand passing anyway. It now expands those constants first, which means it keeps asking what a component renders rather than how it is written. - Popover, dropdown menu, select, switch, checkbox, tabs and accordion read their classes from the shared source. No visual change and no API change: the strings moved to
class-variants.tsso the Vue components render exactly the same markup instead of a copy that drifts. The popover keeps its Radix transform origin; only that one utility differs per framework.tabsListVariantsis now a plain function rather than acvainstance; it takes the same{ variant }argument and returns the same string.
Added
- Charts that are not React-only: `TanStackChart`.
ChartContainerand Recharts are unchanged and stay the default for a React app. The new component wraps TanStack Charts, whose definitions are framework-neutral, so the same chart renders in React, Vue, Svelte, Solid and Angular. That is how the Vue package gets charts at all, since Recharts has no Vue build. - `.vui-chart` maps the theme onto TanStack's palette. TanStack Charts ships no theme of its own. It paints with
currentColorand reads six CSS variables, so one class intheme.cssis enough to make every chart follow light mode, dark mode and a tenant's brand. No colour props, no chart config to keep in sync. The class works in any framework, because it is only CSS.
v1.63
1.63.1
Added
- The theme now ships on its own, for any framework: `@viliha/vui-theme`. The stylesheet was always framework-free, it was just locked inside a React package with thirteen runtime dependencies. It is now published separately with none, in two forms:
theme.cssfor anyone running Tailwind v4, and a compiledvui.cssfor people with no build step at all. Both are generated frompackages/ui/src/theme.cssat publish time, so there is no second copy to drift. Nothing changes for React users; this package still imports the same file. - Two opt-ins so the CSS is not React-only. The bordered icon chip used to key off
svg[width="15"], which is how Radix icons happen to render, so a Vue or Svelte app using a different icon set got nothing..vui-iconnow gets the same treatment. The accordion animation reads--vui-accordion-heightinstead of Radix's variable, andAccordionContentmaps Radix onto it in one line, so a port maps its own library the same way. Both are additive: React output is unchanged. - The npm tarball now carries the license text.
package.jsonsaid MIT but noLICENSEfile shipped with the package, so anyone auditingnode_modulesfound a claim with nothing behind it. The MIT license is now in the package.
Changed
- The README says where the paid tier lives. One line, pointing at internal.viliha.com/pricing. The package itself does not change: it is MIT, it stays MIT, and every version already published is MIT permanently. Pro is separate, optional and additive, so nothing that is free today can move behind it.
- One copyright holder. The license said VILIHA PTE. LTD. while
package.jsonand the README credited a person. Both now say VILIHA PTE. LTD., which is the entity that actually holds the copyright. The license itself is unchanged: still MIT, still free for personal and commercial use.
Fixed
- `init` no longer installs a table library nothing imports.
@tanstack/react-tablewas in the CLI's dependency list, so every scaffolded app pulled it in. No file in the package has ever imported it: the datatable isRecordView, which is hand-written and depends on nothing but React and the icon set. Removed from the install list and from the demo app. - Dependency audit is green again.
postcssin the lockfile was pinned to 8.5.19, which carriednanoid3.3.15 and two high-severity advisories (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8). Refreshing the lockfile movespostcssto 8.5.26, which requires the patchednanoid. Nothing in the package changed; the advisories arrived through a transitive dependency of the demo app.
1.63.0
Added
- An MCP server ships with the package: `npx @viliha/vui-ui mcp`. An agent that wants to know what VUI has can now ask it. Register the server once (
claude mcp add vui -- npx -y @viliha/vui-ui mcp, or the equivalentmcpServersentry in Cursor or any other MCP client) and it gets seven tools: list_guidesandget_guideserve the whole docs site as markdown, all 49 pages from Installation through the per-component reference, so an agent can learn the package end to end without opening a browser.list_componentslists every export with its import specifier, andget_componentreturns one component's source or, when the file is large, its props and exported types plus the docs about it.list_pagesandget_pageserve the reference app's pages, layouts and shell components with their routes, so an agent copies a working page instead of inventing one.search_docssearches all of it,AGENT.md, the README and every guide, and returns the matching sections. No query returns the outline. It ranks with tf-idf and splits sections on bold lead-ins as well as headings, so "organization switcher" returns the paragraph about the org switcher rather than the longest section that happens to say "organization".
v1.62
1.62.1
Fixed
- The tooltip is a light surface like everything else that floats. It was shadcn's dark
bg-primarybubble with white text, which read as a second design system sitting on top of this one, and it became obvious once field help moved onto the label. It now usesbg-popoverwith a border and a shadow, the same surface as menus, popovers, selects and hover cards. The arrow follows, bordered on the two edges facing away from the bubble so it continues the outline instead of drawing a line through it. - One backdrop for every overlay. Dialogs used a themed scrim while alert dialogs, sheets and the command palette painted hard
bg-black, which over an already-dark background is just a muddy rectangle. All four now usebg-foreground/25, so the scrim follows the theme.
1.62.0
Added
- Import and Export are a placeholder you can fill in. The CSV, Excel, JSON and PDF entries still ship and still work in the browser, but they are now built from the same
IoActiontype a host uses, so replacing them or adding to them is the same API rather than a different one. - `IoContext`, what an action gets to work with:
rows(what's on screen, filtered and sorted),columns,title,filefor an import,applyRowsto put records into the table,refetchto reload from the server, and `query`, the active page, sort, search and filters. That last one matters: infetchermode the browser only holds the page someone is looking at, so exporting everything that matches means asking your API with the query rather than writing out the rows in memory. - `pickFile` on an import action opens the file picker and hands the file through
ctx.file, withacceptfiltering what can be chosen. An action without it runs straight away, which is what an API-triggered import wants.
Changed
- The Import menu is hidden on a list with no
makeEmptyRow, since there was nowhere for the rows to go. It was previously shown and did nothing.
v1.61
1.61.0
Changed
- Scrollbars are visible before you scroll. macOS hides overlay scrollbars until something moves, so a table wider than its container looked like it simply ended at the last column, and a long form looked complete at the fold. The datatable now shows both bars as soon as there is anything to scroll to, and so does the add/edit form's body under its pinned footer. The point is to say "there is more this way" before anyone touches the trackpad.
Added
- `.vui-scroll`, the utility behind it, in
theme.css. Put it on any scroll region you add and it gets the same always-visible, theme-coloured bars, thumb inset from the track, darker on hover.
v1.60
1.60.0
Fixed
- A cached table could show the same rows forever. In
fetchermode a cache hit returned early and no request was ever made, and the default TTL was0, which meant "never expires" rather than "don't cache". So a list would keep serving the page it first loaded for the life of the tab, while other people changed the records underneath. Turning keep-alive tabs off didn't help: the cache is module-scoped and had nothing to do with that flag. - Keep-alive off now really means off. Holding a page in memory between visits is the same feature as keeping the page mounted, so it follows the same switch: with
NEXT_PUBLIC_KEEP_ALIVE_TABS=0there is no cross-mount response cache at all. - The default TTL is 60 seconds, not forever. A page older than that isn't painted from cache; the shimmer shows while it loads. A minute-old table read as current is worse than a moment's wait.
Added
- `cache={false}` on
RecordViewturns the response cache off for one table, for the lists where even a moment of last-known data is wrong. - `clearRecordViewCache(cacheKey?)` drops cached pages, one namespace or all of them. RecordView already clears its own after a mutation; call this when something else changed the data: a websocket event, a bulk job, an edit on another screen.
v1.59
1.59.0
Added
- `formRows`: a form is rows, and each row says how many sections it holds. "One column, two rows: two sections on top, three underneath" is now the declaration, near enough word for word:
- `form.errorDisplay` — where a validation message appears,
"tooltip"(new default) or"text"(the old behaviour).
Changed
- Validation shows on the field, not under it. A failing control gets a red border and its message moves onto the field's info icon, so hovering says what is wrong. Nothing is printed under the control any more, because that line of red text pushed the rest of the form down while someone was still typing in it, and nothing is raised as a toast, because a toast is gone before they look. The message is still announced to assistive tech: a border colour and a hover aren't available to everyone. Set
form: { errorDisplay: "text" }to get the old layout back. - Everything that floats now clears what opened it.
Dropdownrendered in place atz-40, so inside a slide-over it was clipped by the scrolling card and sat under the panel: visible sometimes, clickable rarely. It portals to the body now, positioned against its trigger.AlertDialogwas atz-50, under thez-60slide-over, so a confirm raised from a form was unreachable; it is atz-80.Popovermoved to the picker layer,HoverCardabove it, and the datatable's row menu with them.
Deprecated
- `sectionColumns` and `sections[].span` (added in 1.58.0). They force one column count on the whole form, which is the thing rows exist to fix. Both still work and go away in 2.0.
formColumnson a full-page form keeps working too, and now feeds the same row layout.
v1.58
1.58.0
Added
- Form layout, declared rather than styled. A form is a grid of section cards, and every card is the same two columns. Two things to declare, and the alignment follows:
- `sectionColumns` + `sections`.
sectionColumnsflows the cards across the form;sectionsdeclares them, so you can fix their order, let one span the row (span: "full", what a card of long fields wants), or put a line of description under its title. Omitsectionsand they still come from each field'sgroup, as before. A declared section with no fields is skipped, and a group you didn't declare is appended rather than dropped, so adding a field can't make it vanish. A trailing card that would leave a gap stretches to fill its row, unless you declare spans yourself. - Field help is a tooltip on the label. A field with a
descriptionnow shows an info icon before its label, wherever it renders:[i] Label *then the control. That text used to reach only the Info panel on full-page forms, so anyone filling in a slide-over never saw it. The panel is unchanged. - A requirement template for forms: `templates/form.md`, on the docs Templates page. Copy it, fill in the record, the cards and the fields, and hand it to an agent. It asks only what a developer decides, because the layout questions are already settled by the design system.
- A docs page to build against: `/docs/form-layout`. The model, each setting, a worked Add Order form, and how to write field help. Linked from the Customization section of the docs nav.
Changed
- **Every field is one row:
[i] Label *then the control.** Labels are never stacked above their controls, so the eye runs along one line from the name to the box and a form never mixes two rhythms. Hairlines now sit between the two columns and between the rows, light enough to read the grid without drawing the eye. - A form is made wider by putting cards side by side, not by cramming fields into a row. There is no per-field column count, which is what keeps two screens built by two people looking like one product.
Removed
- `fullWidth` on a `RecordField` (added in 1.52.0). It gave a field the whole row with its label above the control, which the layout above rules out, and with a card that is always label │ control every control already fills its column. Delete the prop; nothing else changes.
v1.57
1.57.1
Fixed
- The Add form no longer closes itself on a server-backed table. Clicking "+ {singular}" on a
fetchertable opened the slide-over and it shut again immediately. Opening a blank form was going through the *mutation* path: it invalidated the cache and refetched, the server answered with a page that has no unsaved draft in it, the row the form was editing vanished, and the panel unmounted. Creating a draft and throwing one away are not data changes, so they now update the local rows only. Reproduced on the demo Data Table page. - A background refetch can't close an open Add form either. The draft is held outside the row list, so a poll, a tab refocus or a mutation elsewhere landing mid-edit leaves the form alone instead of taking the record out from under it. Saving puts the record back in the list even if a refetch had already dropped it, so nothing typed is lost.
1.57.0
Added
- `@viliha/vui-ui/org-switcher` — the organization switcher. The brand block at the top of the sidebar becomes a tenant switcher: product mark, product name, and the current organization underneath, opening a list of the organizations the person belongs to with a Current badge, a plan line with a status icon, and a row that creates a new one.
- `orgSwitcher` config section.
heading,currentLabel,addLabel,addHref,showPlanandshowAdd, resolved throughVuiProviderlike everything else, so an app with no billing turns off the plan line once rather than per screen. - Switching organization repaints the app. Each
Organizationcan carry atheme, handed toThemeConfigProvideras the organization layer, so a tenant's brand follows the switch while a personal override still wins. - Demo: the sidebar's dead "Switch workspace" button is now the real thing, backed by
lib/api/workspaces.ts(four tenants, each with a plan and a brand colour) andWorkspaceProvider, with "Add organization" going to the existing/register-businesswizard.
v1.56
1.56.1
Fixed
- Every font in the picker is now actually loaded.
FONT_FAMILIESoffered Geist and Source Serif, but the demo only loaded Inter and JetBrains Mono, so picking either fell through to the generic stack and looked like nothing happened. Both are now loaded withnext/fontand their variables are on<html>, so all four options change the type. - A family that isn't loaded says so.
ThemeConfigProviderchecks in development that the chosen family's CSS variable is actually defined, and warns with the fix when it isn't. A missing font used to fail silently, which is how the above shipped in the first place.
Changed
FONT_FAMILIESdocuments the rule it depends on: every entry names a CSS variable the app is expected to define, so adding a family means loading it in the root layout, not just adding a list entry.
1.56.0
Added
- Terms of Use and Privacy Policy pages. Two public routes,
/termsand/privacy, on a legal shell that reuses the auth screens' brand header and the shared site footer. Unlike the auth screens they are indexable, because people look for them and search engines expect a product to have them: both carry a keyword-led title, description and self-canonical, and both are in the sitemap. - The footer links to both, so every surface that renders
SiteFootergets them: the app shell, the auth screens, the legal pages themselves and the 404/500 pages. The signup form carries the consent line that names them.
v1.55
1.55.0
Added
- Theme configuration: `@viliha/vui-ui/theme-config` + `theme-provider`. The look of the app is now data, so an organization can set its brand and each person in it can override the parts they care about.
THEME_FIELDSis the complete list of what can change (primary colour, text on primary, accent, destructive, background, text, borders, font, text size, corner radius, logo, favicon), each entry naming the CSS variable it writes, the control a settings UI should render for it, and a one-line description. - One brand colour drives the rest.
--brandis the single value a theme sets; the hover state, focus ring, selection colour and button shadow derive from it intheme.csswithcolor-mix, and so does the dark-mode variant. One saved value therefore covers both modes and they cannot drift apart.readableOn()picks black or white text for the chosen colour by WCAG luminance, so a light brand doesn't end up with white text on it. - `THEME_PRESETS` and `FONT_FAMILIES`. Six named colour presets for a swatch picker, and a curated set of self-hosted families. Fonts are loaded by the app (
next/font), so switching one makes no network request and cannot flash or shift the layout. Add a family by loading it and adding an entry. - `parseTheme()` runs on anything from your API. A stored theme is user input that ends up as a CSS variable, so unknown keys, non-strings, and values containing
;,{,}or angle brackets are dropped rather than written to the document. - Demo: Settings → *Theme* has the swatch row, font, text size and corner radius, with the organization's theme as the fallback and a reset. The provider is mounted in
(app)/layout.tsxwithORG_THEMEfromlib/app-config.ts.
v1.54
1.54.0
Added
- `@viliha/vui-ui/page` — `Page`, the standard page frame. The shape every screen copies by hand (full-height column, 48px action header with the breadcrumb trail, then the single scrolling
p-4content region) is now a component with slots:
v1.53
1.53.0
Added
- "Save & New" in one line: `after` on a form action. A saving action can now say what the form does next:
"close"(the default, what Save does),"stay"on the record just saved, or"new"to hand the form a blank record so a run of entries never goes back to the table in between.
Fixed
- A bulk "Set {field}" no longer writes to a field the form won't edit. The bulk actions were built from any field with static
options, ignoringeditable, so a read-only choice column could be changed for every selected row at once. It now needseditableas well, matching what the form allows. If a table lost a bulk action here, the field it wrote to was never meant to be editable: mark iteditable: trueto get it back. - A per-table `behaviour` prop now reaches the form, not just the rows.
confirmDiscardWhenDirtyandcloseOnSaveset on oneRecordViewwere read from the provider only, so a per-table override was ignored inside the add/edit panel.
v1.52
1.52.0
Added
- `formSlots` — your own content between a form's fields. A callout, a preview, a pair of custom controls: each slot renders as a full-width row inside its section, inheriting the card, the separators and the padding rather than floating beside them.
afterplaces one under a named field, in that field's own section;groupnames a section and puts it at the end.renderreceives the live draft, so a slot can react to what is being typed. - `fullWidth` on a `RecordField` — the field takes the whole form row, with its label above a full-width control instead of beside it. For a long textarea, an address block, or a
renderInputthat needs the space.
v1.51
1.51.0
Added
- Behaviour is configurable: `behaviour` on `RecordView` and the provider. Five things that were hard-coded are now keys, each with a real consumer:
rowClick("view" | "edit" | "none", what clicking a record's name does),closeOnSave(keep the form open for the next record),flashMs(the saved-row highlight,0turns it off),confirmDelete, andconfirmDiscardWhenDirty(ask before throwing away unsaved edits, off by default because that is how Cancel has always behaved). The shipped values live invuiPreset, so the components no longer keep private copies of their own defaults. - A user-preference layer: `userConfigurable` + `useVuiPreferences()`. The preconfigured theme stays changeable by the person using the app, not only by the developer who installed it. List the keys an app is willing to hand over (
userConfigurable={{ behaviour: ["rowClick", "flashMs"] }}) and those become the user's, saved per browser and merged over the app's config. Anything not listed is ignored on write, so a stale stored value can't leak back in. This generalises what the top-bar chrome toggles already did for seven flags.useVuiPreferences()returnspreferences,userConfigurable,setPreferenceandresetfor building the settings UI.
v1.50
1.50.0
Added
- A config spine: `@viliha/vui-ui/config`. The preconfigured theme is now itself a config.
vuiPresetis a plain value the package applies by default, built from the same API a host uses, so there is no "configurable" variant of a component sitting beside an opinionated one. Values resolve per-instance prop → `<VuiProvider config>` → `vuiPreset` → package default, and a layer overrides only the keys it mentions, so changing one thing never means adopting a config file for everything. ShipsVuiProvider,useVuiConfig,defineConfigandmergeConfig. The provider is optional: without it you get the theme exactly as before. - Configurable form footer: `formActions`. Cancel + Save (Close + Edit while viewing) are ordinary actions now, so you change them with the API that builds them. Pass a function to start from the shipped list (
formActions={(d) => [...d, archive]}) or an array to replace it. An action takeslabel,variant,icon,align(startpins it left, where destructive actions belong),confirm, andvisible/disabledpredicates reading the live form. ItsonActgetsmode,row(the draft),dirty,valid,errors,close,resetandedit, and may be async, with the footer disabled while it settles. - `renderFooter(ctx)` — replace the form footer outright, for the rare case the action array can't express what a screen needs.
Changed
- The form footer renders through the new action list rather than hard-coded buttons. Nothing moves: the same pair, the same blue Save, the same separators, since the shipped actions are what the preset describes.
v1.49
1.49.0
Fixed
- A save in server mode no longer races the reload. In
fetchermode a mutation invalidated the cache and refetched straight away, before the host's POST/PATCH had landed, so the server answered with pre-write rows and the saved record appeared to revert. Add was the worst of it: the new row showed for a moment and then vanished.onDataChangemay now return a promise, and RecordView waits for it before reloading. If it rejects, the error goes toonErrorand the table reloads anyway rather than sitting on a row that was never stored.
Changed
- `onDataChange` now fires in `manual` and `fetcher` mode too, not only with controlled
data. It is the write hook for server-backed tables: return a promise and the reload waits for it. Inmanualmode (where the host owns the fetch) returning a promise also re-emits the current query throughonQueryChangeonce the write settles, so the page reloads itself after a save. Returning nothing keeps the previous behaviour everywhere.
v1.48
1.48.0
Fixed
- The raw-id rule now covers every component, not just read cells. A
ComboboxorSelectwhose selection hadn't resolved yet fell back to its placeholder, so a set value read as "nothing selected"; the trigger now shimmers until the label lands.MultiComboboxchips listed the stored ids and now shimmer too. Same rule everywhere: a skeleton while resolving, never an identifier. - An unresolvable reference reads `—` instead of `Unknown`. It is missing data, so it now looks like every other missing value rather than a special case, and the id is no longer carried in a tooltip.
Added
- `resolveOptions` batches single-value reads. The batch resolver, until now used only by
multiplefields, works for ordinary async fields as well: every cell of a column painted in the same tick is collected and resolved in one call, so a 50-row Employees page asks for its Department ids once instead of 50 times. With onlyresolveOption, identical ids already in flight are shared, which still removes the repeats. - `displayValue(row)` on `RecordField` — skip resolution entirely. When your payload already carries the label next to the id (
{ countryId, country }), return it and the read display paints instantly with zero requests. Unlikerenderit supplies only the text, so the cell keeps its alignment, truncation and copy button, and the edit control is untouched.
v1.47
1.47.0
Fixed
- Async fields no longer paint the raw id before the label arrives. A read cell, detail panel or profile row backed by
loadOptions+resolveOptionused to render the stored value first, so a page opened asRegion 1 · Country 1 · Timezone 27and swapped to real labels as each fetch landed. On a throttled connection those integers sat there for seconds. Read displays now show a skeleton until the label resolves. - A reference that can't be resolved reads `Unknown`, not its id. If
resolveOptionfails or returns nothing, the cell saidCurrency 142forever with no sign anything had gone wrong. It now readsUnknownwith the id in the tooltip, so a dangling foreign key looks like an error and stays debuggable. Applies tomultiplefields too. - Identical resolves in flight at the same time are shared. Every rendered value used to fetch on its own, so a table of 50 rows on the same country id fired 50 requests. They now collapse into one. The entry is dropped as soon as it settles, so nothing is held long enough to go stale.
Added
- `useAsyncOptions` returns `resolving` — true while a set value's label is being resolved (distinct from
loading, which covers the dropdown's own list fetch). Use it to show a skeleton instead of a value your reader can't read.
v1.46
1.46.0
Fixed
- A read-only table no longer offers a row Edit button. The pencil rendered on every row whenever you weren't in Trash, without ever looking at the fields. On a list where nothing is
editable(a catalog mirror, a subscribe-only list, the demo Users table) it opened a form with no body and a Save that committed nothing. Edit now follows the fields: no editable field, no pencil and no Edit button on the view panel.
Added
- `showEdit` — the row Edit toggle, alongside
showAdd/showFilter/showSortand the rest. It's the one that doesn't default totrue: left unset it follows the fields, as above. PassshowEdit={false}to drop Edit from a table that does have editable fields, orshowEditto force it on.onEditstill redirects the action rather than removing it.
v1.45
1.45.0
Added
- `BrandAsset` hands you the `File` instead of a base64 string. Pass
onPick(file), upload it wherever you keep assets, and return the URL to show ({ url }or a plain string, or nothing if you drivevalueyourself). The control awaits the promise, shows its own "Uploading…" state, and surfaces a rejection as an inline error. Nothing is uploaded or fetched by the package. - `BrandAsset` props: `onRemove`, `meta`, `accept`, `maxBytes`, `busy`, `inline`.
metaprints a details line under the preview (name · format · dimensions · size · date; dimensions are read off the rendered image when you don't supply them).acceptnow defaults to every image type instead of a fixed PNG/JPEG/SVG list.maxBytesrejects an oversized file beforeonPickis called.busylets a host drive the uploading state during a save. - `orgProfileFields(options)` — the organization profile preset with your uploader wired into the Logo and Favicon fields:
orgProfileFields({ logo: { onPick }, favicon: { onPick } }). Every other field is untouched, so a profile screen no longer needs a local replacement control just to store an image properly.
Changed
- `BrandAsset` no longer writes a base64 data URI by default. Storing the image inline meant a 2 MB logo became a ~2.7 MB field value: too long for a normal column, invisible to a CDN, and impossible to cache or transform. The data-URI path is still there for demos with no backend, but you now ask for it with
inline. A control with neitheronPicknorinlinesays so instead of quietly producing a value that cannot be saved.organizationProfileFields(the prebuilt demo preset) keepsinline, so the bundled Organization Profile page behaves as before.
v1.44
1.44.1
Fixed
- Chat: long messages now wrap instead of overflowing. A message made of one unbroken string (a long URL, a pasted token, keyboard mashing) stretched its row and pushed a horizontal scrollbar onto the whole conversation. The message text now breaks mid-word, so the chat column stays inside its width at any message length.
1.44.0
Added
- `@viliha/vui-ui/multi-combobox` — `MultiCombobox`. A searchable multi-select (the multi companion to
Combobox): holds astring[], renders the selection as removable chips, and picking an option toggles it while the popover stays open. Staticoptionsor an asyncsource(fetch on open, debounced search, and batch label-resolve of the selected values). - Multi-select `RecordField` (many-to-many form fields). Set
multiple: trueon a choice field and its value becomes astring[]: the Add/Edit form renders the multi-select with chips, and the read cell shows up tomaxChipsInCell(default 3) labels then "+N" with the full list in a tooltip. Async fields add a batchresolveOptions(values)(the plural companion toresolveOption).requiredmeans at least one selected (blocks Save). Reuses the existingloadOptions/dependsOncontract; nothing loads on mount. Demo: Markets → Post Codes. - `AsyncOptionSource.resolveOptions` —
useAsyncOptionsnow batch-resolves set values in one call when a source provides it (falls back to per-valueresolveOption).
v1.43
1.43.0
Added
- Trash mode — view soft-deleted rows and restore them. Set
showTrashto add a Trash toggle in the header (left of Filter) that switches the same table between live and soft-deleted rows. RecordView is display-only: the host supplies the trashed rows (trashedDatain client mode, or the newtrash: trueflag onServerQueryformanual/fetchermode) and persists restores via `onRestore(rows)`. Restore mirrors delete: a per-row Restore icon and a bulk "Restore N selected" via the selection checkbox, each with a confirm dialog; on restore RecordView clears the selection and refetches (manual) / expects the host to drop the rows fromtrashedData(client). In Trash the Add button is hidden and row Delete becomes Restore. Demo: the Branches table.
v1.42
1.42.1
Changed
- A built-in `input:"checkbox"` now coexists with `render`. Like a custom
renderInput, a field can pair arender(the read view — e.g. a status badge in the table) withinput:"checkbox"(the edit control): the form shows the badge while viewing and the checkbox while editing. The Branches HQ column uses this — badge in the list, checkbox in the Add/Edit form.
1.42.0
Added
- Field-level validation in the Add/Edit form (slide-over and full-page). A
RecordFieldnow takes declarative rules:min/max(character length, or numeric value forinput:"number"),pattern(regex +patternMessage),format: "email" | "phone", a customvalidate(value, draft), andtrim. Rules run on blur and before Save, block Save while any field is invalid, and show the message inline under the field."phone"also auto-formats the value as(123) 456-7890while typing. Previously the form enforced onlyrequired(and Save wasn't blocked beyond that). - `input: "checkbox"` — a boolean field renders a real checkbox in the form and shows Yes/No in read/view and table cells.
v1.41
1.41.1
Fixed
- Async choice fields now show their label in read/view mode, not the raw id. A
RecordFieldwithloadOptions/resolveOption(its value is an id) rendered the id ("5") in read mode while edit mode showed the correct title. The read display now resolves the label viaresolveOption(one record, never the whole list) everywhere a value is shown — form read rows, detail panels and table cells — falling back to the id while it resolves. Static-optionsfields already mapped to their label; this brings async fields in line. The picker itself was never affected.
1.41.0
Added
- `NEXT_PUBLIC_PASSWORD_MASK` — app-wide default for `PasswordInput`'s mask. Set it to
nativeto make every password field use the browser's bullet-dottype="password"(so password managers and autofill work), or leave itasterisk(default) for the*overlay. The per-fieldmaskprop still overrides it. Declare the var inturbo.jsonglobalEnvif you readNEXT_PUBLIC_*insidepackages/ui. Documented on the Configuration docs page.
v1.40
1.40.0
Added
- `@viliha/vui-ui/profile-form` — `ProfileForm`. A pre-designed profile page you import, like
Steps. Feed itfields(grouped into sections via each field'sgroup) anddata, and it renders the whole thing: a read-only view, an Edit button that opens the standard Cancel + Save footer, revert on Cancel, the About info panel, and a loading skeleton. No page boilerplate. - `@viliha/vui-ui/organization-profile` — organization preset. Ready-made field definitions for a company profile:
organizationProfileFields(Org information, Brand assets, Contact & address, Localization), theOrgProfiletype,getOrgPrimary,ORGANIZATION_PROFILE_DESCRIPTION, and the reusableBrandAssetlogo/favicon control. Spread or override the fields to fit your schema. The backoffice/organization/profilepage is now a thin consumer of both.
v1.39
1.39.0
Added
- Form sections take any title. A
RecordField'sgroupaccepts any string now, not just the four built-in names, andRecordFormrenders one section per group in the order the groups first appear. So a form can read "Organization information", "Brand assets", "Contact & address" instead of "General"/"Work". Ungrouped fields still fall under "General"; existing forms are unchanged.
Changed
- A field can have both a custom view and a custom edit control. When a field sets both
render(read-only view) andrenderInput(edit control),RecordFormnow showsrenderwhile viewing andrenderInputwhile editing — so a cell like a logo preview or a status badge stays editable in Edit mode. Before,renderalways won and the edit control never appeared. Fields with only one of the two behave exactly as before.
v1.38
1.38.0
Added
- User-resizable columns are on by default.
RecordView'sresizableColumnsnow defaults to theNEXT_PUBLIC_RESIZABLE_COLUMNSenv var, which is on unless you set it to0orfalse. Drag a column's right edge to widen it, so a long value in a narrow column is always reachable. PassresizableColumns={false}on a single view to opt out. Declare the new var inturbo.jsonglobalEnvif you readNEXT_PUBLIC_*insidepackages/ui.
Fixed
- Custom-rendered cells no longer overflow into the next column. A cell with a
renderfunction (for example a long status badge likePARTIALLY_REFUNDED) clipped to its own column now, matching how plain text cells already truncate. Before, the badge could spill across the column border and cover the neighbouring value. Widen the column by dragging its edge or set the field'swidth.
v1.37
1.37.1
Fixed
- `RecordView` server (`manual`) mode now reflects writes and filters without a manual reload. When a host drives the table server-side and feeds each page through
initialData(rather than the controlleddataprop), RecordView seeded its rows once and never reconciled them. So after a create, edit, or delete, the list kept showing the old rows, and typing a Filter value left the full catalog on screen. RecordView now re-syncs its internal rows whenever that seed changes, so a post-mutation refetch or a narrowed filter shows up right away. The controlleddatapath and client-managed tables are unchanged. - Filtered-empty tables read correctly. With an active per-field filter and no keyword, an empty result now says "No matching records." instead of the first-run "No records yet."
1.37.0
Added
- `PasswordInput` `mask` prop (`"asterisk" | "native"`, default `"asterisk"`).
"asterisk"keeps the*overlay;"native"renders a realtype="password"(bullet dots) that the eye toggle flips totype="text", so browser and password-manager autofill work normally. Pick per usage.
v1.36
1.36.0
Added
- `@viliha/vui-ui/password-input` — `PasswordInput`. A password field that masks with **
*(not the browser's bullet dots) and adds an eye toggle** to reveal the value. Drop-in forInputinside aField: spreadbind(...)and passerrorfor inline validation, and it owns its right edge so the reveal button and the error icon never collide. It keeps the real value in a text input (native typing/paste/caret) with a monospace asterisk overlay, so browser password-manager autofill won't recognise it — use a plain<Input type="password" />when native autofill matters more than the asterisk look. Props:error,maskChar(default"*"), plus all<input>props.
Changed
- Auth screens use `PasswordInput` for every password field (sign in, reset password): asterisk mask + show/hide, wired to
useFormFields.
v1.35
1.35.1
Fixed
- `use-form-fields` — memoize the internal
runhelper sobindandvalidatesatisfyreact-hooks/exhaustive-deps(the package lints with--max-warnings 0, so the warning failed CI). No behaviour change.
1.35.0
Added
- `@viliha/vui-ui/use-form-fields` — inline field validation as a feature.
useFormFields(rules)validates textInput/Textareafields on blur and on submit, surfacing every error through a single channel: the field's own inline error. Spreadbind(key)onto the control, readerrors[key], callvalidate()on submit, and setnoValidateon the<form>so the browser's native bubble can't compete. Rules receive all field values for cross-field checks (e.g. confirm-password). Returnsvalues,errors,bind,setValue,setError,validate,reset.
Changed
- `Field` (`@viliha/vui-ui/field-grid`) now renders the inline validation error itself — red border via the control's
aria-invalid, an alert-triangle tooltip, no layout shift, auto-clear on edit, re-check on blur/submit. Works forInputandTextarea(multilineputs the icon at the top). The reference app's authFieldnow re-exports this one.
Fixed
- Auth screens: no more double validation. Sign in / up and forgot / reset password set
noValidateand validate throughuseFormFields, so the browser's native "Please include an '@'…" bubble no longer fires on top of the theme's inline error. Email now validates the moment you leave the field.
v1.34
1.34.3
Changed
- Filter panel — tighten the gap between a field's label and its control.
FilterGrid's label column wasminmax(4.5rem, max-content), so short labels like "Name" or "Code" were padded out to 4.5rem and left a wide gap before the input. The column is now plainmax-content, so it sizes to the label and the control sits right beside it. Labels still align across rows.
1.34.2
Changed
- README + npm metadata — humanize pass and SEO. Rewrote the opening and
package.jsondescriptionaround real search terms (React component library, admin dashboard template, Next.js, Tailwind CSS v4, shadcn/ui, datatable), expandedkeywords, and removed every em dash per the new humanize rule. Mirrors the repo README. Docs/metadata-only.
1.34.1
Changed
- README — rewrote the Sponsor section in a plainer, human voice: dropped the stacked superlatives and marketing phrasing, kept the ask (free under MIT, real effort, even $1 helps, sponsors wall). Mirrors the repo README. Docs-only.
1.34.0
Added
- `@viliha/vui-ui/wizard` — a multi-step wizard layout scaffold.
Wizardrenders the stepper (fromsteps+current), a scrolling body for the active step, and a Back/Next footer;WizardSectionis a bordered title/icon section. You own the step index, field state, and all logic and drop any components inside — it's layout only, not a data-driven form. - `@viliha/vui-ui/field-grid` — `FieldGrid` + `Field`. The form design standard: **two columns,
Label *│ control on one row**, labels left-aligned and sized tomax-content(no dead space) and aligned across rows. Supportsrequiredandhint. Used by the wizard sections and any form that wants the standard two-column layout.
Changed
- Reference `/steps` page rebuilt on
Wizard/WizardSection/FieldGrid: fixes the oversized label→control gap (was a fixed 140px label column) and demonstrates one step with multiple sections.
v1.33
1.33.1
Changed
- README — lead with a prominent Sponsor section at the very top: Vui is a free, enterprise-grade theme that would normally sit behind a paywall, built with significant personal effort, and sponsorship is what keeps it free and maintained. Mirrors the repo README. Docs-only.
1.33.0
Added
- `@viliha/vui-ui/cascading-combobox` — `CascadingCombobox`. A cascading picker for fixed, named levels (Region → Country → State → City): one searchable
Comboboxper level, where picking a level narrows the next from the selected node'schildrenand clears everything downstream. A level is disabled until its parent is chosen. Data-driven — pass aCascadeNode[]tree of any depth (3, 4, N levels). Exported types:CascadeNode,CascadeLevel. - `Combobox` `disabled` prop — inert trigger, popover can't open (used per level by
CascadingCombobox).
v1.32
1.32.0
Added
- `@viliha/vui-ui/filter-field` — composable filter layout primitives.
FilterGrid+FilterFieldrender the theme's enforced filter layout — two columns: label │ control, one row per field, labels aligned across rows. Anyone can add a labelled control to a filter panel with them, and the design can't be styled away. - `RecordView` `filterExtras` prop — inject your own
FilterFieldrows into the Filter panel (below thefilterablefields, in the same grid). The panel now opens when there arefilterablefields orfilterExtras.
Changed
- `RecordView` Filter panel is now two-column by default (label │ control on one row), replacing the stacked label-above-control layout. Built on the new
FilterGrid/FilterField, so the layout is consistent and enforced whether fields come fromfilterableconfig orfilterExtras. Panel widened to fit.
v1.31
1.31.0
Added
- `@viliha/vui-ui/auth-context` — a provider-agnostic auth contract. Exports
AuthProvider,useAuth(), and theAuthContract/AuthUser/Credentials/SignUpInputtypes. Auth screens now depend on this small interface rather than any provider SDK, so wiring a real backend (NextAuth, Clerk, Better Auth, Supabase, …) is a single adapter with no screen changes.useAuth()throws when noAuthProvideris mounted, so a missing adapter fails loudly instead of silently no-op'ing.
Changed
- Reference app — auth screens (
signin,signup) and sign-out are wired touseAuth(); the app mounts a Better Auth adapter (app/_components/auth-provider.tsx) that activates whenNEXT_PUBLIC_AUTH_BASE_URLis set and otherwise falls back to an in-memory mock so the static demo works with no backend. Docs at/docs/auth.
v1.30
1.30.2
Changed
- README — republish so the "Support VUI" sponsor section, badge, and acknowledgments actually reach npmjs (they were bundled with a version already on the registry). No component changes.
1.30.1
Changed
- README — add a GitHub Sponsors badge/link, a "Support VUI" section, an acknowledgments section (inspired by / thanks to shadcn/ui + React), and emphasize the live demo app. Docs-only; no component changes.
1.30.0
Added
- shadcn/ui components, batch 4 — completes the set. Ported into the package (Radix-based, VUI tokens):
aspect-ratio,breadcrumb,calendar,command,form,progress,scroll-area,sonner. New deps:cmdk(command),react-day-picker(calendar),react-hook-form(form),sonner. Adaptations to fit VUI:commandships the cmdk primitives withoutCommandDialog(VUI'scommand-paletteis the modal ⌘K menu);sonner'sToasterreads the.darkclass directly instead ofnext-themes;calendaruses VUI'sButton/buttonVariants;theme.cssalready carried the tokens these need.
v1.29
1.29.0
Added
- shadcn/ui components, batch 3 (form controls). Ported into the package (Radix-based, VUI tokens):
textarea,radio-group,slider,toggle,toggle-group,input-otp. New dep:input-otp(for the OTP field);theme.cssnow ships thecaret-blinkkeyframe so the OTP caret animates withouttw-animate-css.
v1.28
1.28.0
Added
- shadcn/ui components, batch 2 (overlays). Ported into the package (Radix-based, VUI tokens):
alert,alert-dialog,collapsible,hover-card,popover,sheet. No new dependencies (all use the existingradix-ui/class-variance-authority/lucide-react).AlertDialogAction/AlertDialogCancelstyle the Radix element with VUI'sbuttonVariants(VUI'sButtonhas noasChild), so they still accept Buttonvariant/size.
v1.27
1.27.0
Added
- shadcn/ui components, batch 1 — now in the package. Ported (Radix-based, restyled to VUI tokens, zero token duplication) so consumers import from
@viliha/vui-ui/<name>instead of runningshadcn add:accordion,label,separator,skeleton,switch,tabs. New runtime deps:radix-ui,class-variance-authority,lucide-react.theme.cssnow ships theaccordion-down/accordion-upkeyframes so the Accordion animates withouttw-animate-css. More components in later batches.
v1.26
1.26.2
Changed
- Scaffolded auth screens (shipped in
template/, from the reference app): sign-in and sign-up now lead with the email/password form and place the Google / passkey / SSO options below an "or" divider, for a consistent layout. All auth titles, buttons, labels, and copy are Title Case. Fields render label-beside-input in a two-columnFieldGrid, and validation errors show as a red border + tooltip that auto-clears on edit (no layout shift). No component API change —src/is unchanged.
1.26.1
Fixed
- README blank on npmjs.com — resolved by republishing. The registry had the README correct all along (top-level packument
readmefield, which npm populates on its own); the blank page was an npmjs.com re-index delay. Publishing a new version makes npmjs re-render it. No source or tooling change is needed for this. (This release also briefly added a manifestreadmeinjection topublish.mjs; it had no effect and was removed the same day — the per-versionreadmefield is stripped by the registry for every package.)
1.26.0
Added
- `RecordView` toolbar feature toggles. New boolean props let a page choose which toolbar controls appear — each defaults to on, so existing tables are unchanged:
showFilter,showSort,showPagination(the standard set),showImport,showExport(turn off per page — they "depend on the user"),showAdd, andshowSelection.showPagination={false}renders all rows (no page slicing) in client mode;showSelection={false}removes the checkbox column, bulk actions, and Clear selection (and, since they share the leading column, drag-to-reorder).
Fixed
- README missing on npmjs. npmjs.com renders the README from the per-version
readmefield in the publish request, whichpnpm publish(and yarn-classic / bun) do not send — onlynpm publishdoes. Publishing now goes throughscripts/publish.mjs(run it withpnpm releaseornode scripts/publish.mjs, from any package manager): it strips the internalworkspace:*devDeps to keep the manifest spec-valid, then uploads vianpm publish, so the README shows again. AprepublishOnlyguard blocks a bare<pm> publishand points at the script.
v1.25
1.25.2
Added
- `Input` invalid state — an
<Input>witharia-invalidnow paints its border and focus ring in thedestructivecolor, so form validation can show an error without a layout-shifting message line (pair it with a tooltip for the message). The[aria-invalid]selector out-specifies the base border, so no!importantis needed.
Fixed
- `Toaster` hydration mismatch — the portal was gated on
typeof document === "undefined", which isfalseduring client hydration, so the toast region rendered on the client but not the server. Now gated on a mounted flag, so the first client render matches the server (null) and the portal mounts in an effect.
1.25.1
Added
- Revalidate stale keep-alive tabs (`useRefetchOnActive`) — a scaffolded hook (
lib/use-refetch-on-active.ts) that re-runs a controller'srefetchwhen its tab becomes active again or the window regains focus, so a kept-alive page no longer shows data frozen at open time after another user edits a record. The reference wiring (use-organizations.ts+ the data layer'ssyncOrganizations(since)) revalidates with a delta (?since=cursor→ only changed rows + deleted ids, merged byid), not a full reload. Documented inAGENT.md(Open tabs) and docs/layout.
1.25.0
Added
- Async option loading for pickers (`loadOptions` / `resolveOption`) — a reusable primitive so
Combobox,Select, andRecordViewchoice/filter fields fetch their options on demand instead of receiving a fully-loaded staticoptionsarray. Browsing a table now fetches zero reference data; a picker loads its list only when opened, and an already-set value shows its label from a single-recordresolveOption(never the whole list). Powered by the exporteduseAsyncOptionshook. - `Combobox` / `Select` accept a
source({ loadOptions, resolveOption }) in place ofoptions, plusresetKeyfor cascade invalidation. Combobox debounces server search (250 ms, aborts superseded requests); both surface loading / empty / error (retry) states inside the dropdown. - `RecordField` / `FieldFilter` gain
loadOptions({ search, signal, values }),resolveOption(value), anddependsOn— so form and filter comboboxes lazy-load, withvaluesgiving the current draft / filter values anddependsOnclearing the cache + selection when a cascade parent changes. Works whereonFormOpen(1.24.0) couldn't reach: filters and standalone pickers. - Exported types:
AsyncOptionSource,AsyncOption.
v1.24
1.24.0
Added
- `onFormOpen?(mode, row?)` on
RecordView— fires when the Add / View / Edit form opens (modeis"create" | "edit" | "view"), so you can lazily load field data only when a form actually opens instead of on every table mount. The intended use is FK / combobox option catalogs (e.g. Region → Country → State cascades): browsing the list fires nothing, opening a form triggers the fetch. It's a pure notification — unlikeonCreate/onView/onEdit(which redirect and suppress the panel),onFormOpendoes not suppress anything. In panel moderowis the record opened (the fresh draft for"create"); in page mode it fires alongside the redirect for symmetry.
v1.23
1.23.2
Changed
- Publish with `pnpm publish` (now enforced). The published manifest used to ship internal dev tooling as literal
workspace:*(an invalid npm manifest, left there bynpm publish).pnpm publishrewrites those to spec-valid versions, so aprepublishOnlyguard now blocks any non-pnpm publish. Runcd packages/ui && pnpm publish.
Note
- This does not change the npmjs "no README" display — that's a registry-side render issue (the README is present and valid in every tarball regardless of publish client); it needs an npm support ticket, not a republish. Corrects the 1.21.2 / 1.23.1 notes that wrongly attributed it to the publish client.
1.23.1
Fixed
- README still not rendering on npmjs. Confirmed the published tarball contains a valid
README.mdand npmjs renders from the publish payload, whichpnpm publishleaves empty (the registry'sreadmefield was frozen at a stale, older copy while the tarball was current). Publish this version with `npm publish` from `packages/ui` (notpnpm publish, not a workspace filter) so the README is in the payload. No code change.
1.23.0
Added
- Env-driven page size for server-paginated tables.
RecordViewnow readsNEXT_PUBLIC_DEFAULT_PAGE_SIZE(initial rows per page) andNEXT_PUBLIC_MAX_PAGE_SIZE(the ceiling the page-size selector won't exceed), both overridable per table via the new `defaultPageSize` / `maxPageSize` props. Infetcher/manualmode the data layer must clamp its returned page to the max too — the client's requested size isn't trusted. - `users` demo — server-side pagination over a large table. A new reference showing the correct pattern for a table with far more rows than you'd send to the browser:
lib/api/users.tsreturns one page at a time (search / sort / filter / paginate on the server, page size clamped to the max), and the view wires it withRecordView'sfetcher. Only one page is ever in memory.
Changed
- `initialData` and `makeEmptyRow` are now optional. Omit them for a
fetcher-backed or read-only list; the "+ New" button (and CSV/JSON import) is hidden when a table has no way to create rows. Client-managed tables are unaffected (they still pass both).
v1.22
1.22.0
Added
- Three-layer architecture for data-backed pages (Data → Controller → Presentation), demonstrated end-to-end on the scaffolded
organizationspage and documented inAGENT.mdand the docs site. Data access lives inlib/api/<entity>.ts(async, no React — the real-API seam), a controller hookuse-<entity>.tsowns{ data, loading, error }and loads *after mount*, and the presentation rendersRecordViewwith no data processing of its own. - UI-first navigation. The presentation now paints its shell first: a thin
*-table.tsxnext/dynamic-loads the datatable view behind aTableSkeleton, and the controller startsloading: truewith no rows — so the skeleton shows the instant you click a menu item, instead of waiting for the datatable chunk to parse and the data to arrive. Fixes the perceptible lag before the loading animation on record-heavy pages.
v1.21
1.21.2
Fixed
- README now renders on npmjs. Root cause:
pnpm publishpacksREADME.mdinto the tarball but doesn't set the registryreadmefield the website renders (it was empty for 1.21.0/1.21.1). Publish the package with `npm publish` (frompackages/ui), notpnpm publish. No code change.
1.21.1
Fixed
- List
README.mdin the packagefilesso npm reliably renders the README on the package page (npmjs was showing a stale/empty README — the registry's readme field wasn't refreshing on publish). No code change.
1.21.0
Added
- `Toaster` + `toast()` (
@viliha/vui-ui/toast) — global notifications for errors and events, matching the shadcn toast design (title, description, an action button like "Undo", close, andsuccess/error/warningvariants). It's a module store, sotoast(...)works from anywhere — event handlers,catchblocks — with no provider; mount<Toaster />once in the root layout (the scaffold now does). Dependency-free (portal + fixed positioning, likeTooltip), bottom-right stack with a rise-and-fade entrance.
v1.20
1.20.0
Added
- `identityColumn` prop — position (or hide) the identity column. The leading Name/Title column was hard-forced first; it can now go anywhere among the field columns:
"first"(default),"last","hidden", or a number = how many field columns precede it (e.g.1→ Region, Title, Code). Lets reference/data-grid tables order columns freely (Country → State → City → Title). The header, loading skeleton, and body rows all render from one shared ordered column list, so they stay in sync. Default"first"keeps existing tables unchanged.
v1.19
1.19.0
Added
- Sortable identity column header. The leading Name/Title column (driven by
nameLabel+getPrimary) was a static header — you could only sort it from the Sort dropdown. It now toggles sort on click and shows the same CaretSort/CaretUp/CaretDown affordance as other columns. It sorts bynameSortKey, or auto-detects the firsthideInTablefield markedsortable(the one drivinggetPrimary). No opt-in beyond marking that fieldsortable; if none is sortable, the header stays static (unchanged).
v1.18
1.18.0
Changed
- `Tooltip` now matches the shadcn design (dark
bg-primarybubble + arrow) and takes a `side` prop:"top" | "bottom" | "auto"(default"auto"). Auto prefers bottom and flips to top only when there isn't room below — so datatable cell tooltips drop below a column but flip up for the last rows near the footer, never clipped. Still dependency-free (portal + fixed positioning). RecordView's truncated-cell tooltips get this automatically.
v1.17
1.17.0
Added
- `nameLabel` prop on `RecordView` — the leading identity-column header was hardcoded to "Name". Set
nameLabel="Title"(etc.) for tables whose identity is a title field (regions, countries, languages, roles, …). Default stays "Name", fully backward compatible.
v1.16
1.16.0
Added
- Configurable page-form breadcrumb.
RecordForm(page layout) takes acrumbsprop that fully replaces the defaultHome › {title} › Create/Update {singular}trail — add parents (e.g. an "Access" section), rename the last crumb ("New Role"), or reshape it per route. Each crumb is{ label, onClick? }; the last is the current page. TheCrumbtype is re-exported from@viliha/vui-ui/record-view(also in@viliha/vui-ui/breadcrumbs).
Changed
- Choice-field cells show the option label, not the raw value. A field with
optionsnow renders its matching label in the table (e.g.SYSTEM→ "System") while staying editable — no need for a read-onlyrenderjust to map an enum to a friendly label. Falls back to the raw value when unmatched.
v1.15
1.15.0
Added
- Cell truncation in `RecordView` — long text stays on one line. A cell longer than
maxCellCharsis clipped with an ellipsis (…) and shows the full value in a styled hover tooltip, so a sentence never wraps to a second row. The limit defaults toNEXT_PUBLIC_MAX_CELL_CHARS(or 25) and is overridable per view (maxCellCharsprop) or per column (a field'smaxChars;0= never truncate). Applies to the identity/Name cell and every value column. - `Tooltip` (
@viliha/vui-ui/tooltip) — a lightweight, dependency-free themed tooltip (portal + fixed positioning, hover/focus, auto-flip). Used by the cell truncation above; reusable anywhere.
v1.14
1.14.2
Changed
- Consistent loading feedback in `fetcher` mode. A cache hit is served from the in-memory cache (no server round-trip), but the loading shimmer now stays up for a short minimum (300ms) so a cached load looks the same as a real fetch — no confusing "loading but blank" flash on tab switch. Real fetches longer than the minimum are unaffected; background (post-mutation) refetches stay silent.
1.14.1
Fixed
- Per-field Filter panel layout. The header ("Filter") and the footer (Clear / Search) are now static — only the fields scroll — so the actions stay in view on a long filter form. The footer's top border spans the full panel width, and the action buttons are compact (
size="sm").
1.14.0
Added
- Built-in server data source for `RecordView`: `fetcher` + query cache. Pass
fetcher={(query, signal) => Promise<{ rows, total }>}and RecordView owns the whole read path — it fetches on every query change, managesdata/rowCount/loading, aborts superseded requests via thesignal, and (withcacheKey) caches responses so returning to a tab is instant with no refetch. Combine withpersistKeyto restore page/sort/filters on remount and hit the cache. New props:fetcher,cacheKey,cache({ max, ttlMs }LRU),onError. Mutations are optimistic → invalidate → background refetch. This replaces the hand-wireddata/onQueryChange/loading+Mapboilerplate (still supported as the lower-level API). The Data Table demo now uses it. - Changelog on the docs site at
/docs/changelog, rendered straight from this file at build time (single source, never drifts). Linked in the docs nav.
v1.13
1.13.2
Fixed
- Data Table demo no longer reloads on tab switch. Keep-alive keeps a page mounted, but under the App Router an async (server-fetching) page can still remount on tab switch and re-run its fetch. The demo now caches fetched pages in a module-scoped
Mapkeyed by the query (served synchronously, no shimmer) and passespersistKey, so returning to the tab restores the exact page/sort/ filters and shows the data instantly — no round-trip.
1.13.1
Fixed
- Keep-alive tabs now truly preserve a page's live state across tab switches (within
NEXT_PUBLIC_MAX_TABS). The scaffold'sKeepAliveTabswas overwriting a route's cached element with Next's freshchildrenon every re-activation, which remounted the page and threw away its state — so a server-backed table (e.g. the Data Table demo) refetched every time you returned to its tab. It now caches each route's element once and reuses it, so switching away and back keeps the loaded data, scroll, and form state with no reload. Query-param routes (/organizations/edit?id=…) still update via their ownuseSearchParams.
1.13.0
Added
- Server-side ("manual") mode for `RecordView`. Set
manualand RecordView stops filtering/sorting/paginatingdata— it renders it as the current page verbatim and reports the query viaonQueryChange({ page, pageSize, sort, search, filters })so your backend does the work. Pair withrowCount(drives the footer + page count) andloading. Fires once on mount for the initial load; per-field filters fire it on the Filter panel's Search/Clear. New exported types:SortState,ServerQuery<T>. Default off — everything stays client-side. - Data Table demo page in the scaffold (shadcn/ui section) — a full server-side table against a simulated backend (pagination, sort, keyword + per-field filter, loading shimmer, out-of-order-response guard).
Changed
- The loading skeleton now shimmers left → right (a
vui-shimmersweep intheme.css) instead of a pulse — a clearer "records are loading" cue.
v1.12
1.12.0
Added
- `RecordView` `loading` prop — while
true, the table body renders animated skeleton rows (matched to the columns) instead of an empty-state flash, for slow server loads (initial fetch or refetch). The toolbar stays usable; clearloadingwhen the data arrives.
v1.11
1.11.1
Changed
- Sort indicators are now carets, and every sortable column shows one. A sortable column header shows a muted up/down caret (
CaretSort) by default — so sortability is discoverable — and a solid caret for the active direction:CaretUp= ascending,CaretDown= descending (all Radix icons). The Sort dropdown matches. Non-sortable columns (sortable: false) show no indicator.
1.11.0
Added
- Dependent (cascading) options in `RecordView` — a choice field's options can now depend on another field's current value, in both the Add/Edit form and the Filter panel. Opt-in, backward compatible.
- Form:
RecordField.optionsmay be a function of the draft —((draft) => { value, label }[])— recomputed as the draft changes. - Filter:
FieldFilter.optionsmay be a function of the current filter values —((values) => { value, label }[]).FieldFilteris now generic (FieldFilter<T>). - Auto-clear: when the parent changes and the child's value is no longer a valid option, RecordView clears it (form and filter). Static-array options are unchanged; the bulk "Set {label}" action only lists static-option fields (no single draft to resolve a function against).
v1.10
1.10.0
Added
- `Combobox` (
@viliha/vui-ui/combobox) — a searchable single-select. Same API asSelect(drop-in) but the popover leads with a type-to-filter input, so it scales to long option lists (an FK / country picker). Keyboard: type to filter, ↑/↓ to move, Enter to pick, Esc to close. - `RecordView` choice fields can use the Combobox. In the Add/Edit form set
input: "combobox"on anoptionsfield for a searchable control (default staysSelect). In the Filter panel,control: "combobox"now renders the real searchable Combobox (previously it fell back to a plain Select). - `RecordField.renderInput` — an escape hatch to render any Add/Edit control (checkbox, radio group, slider, date-range, a custom widget). It overrides the default control and receives
{ value, onChange, field, invalid }; the field still owns the label, required mark, and Save validation. The Organizations form demonstrates it (Status as a radio group).
v1.9
1.9.0
Added
- `sortable` field flag in `RecordView` — decouples sorting from column visibility. By default a field is sortable iff it's a visible column (
!hideInTable), unchanged. Now: sortable: truesorts a field with no column (e.g. ahideInTablename shown viagetPrimary) — it appears in the Sort dropdown.sortable: falsekeeps a visible column unsortable (its header stops being a sort toggle).
v1.8
1.8.0
Added
- Per-field filtering in `RecordView` (opt-in, backward compatible). Mark a field
filterableand the Filter panel switches from the single keyword box to a labeled control per field plus Search / Clear. The control is dynamic so the front end can compose a different filter form per request: filterable: true→ a text input.filterable: { control, label, placeholder, options }→ pick the control:"text" | "number" | "date" | "select" | "combobox" | "checkbox"(unknown or omitted → text).optionsfalls back to the field's ownoptions.- New exported types:
FilterControl,FieldFilter,FilterValues<T>. - The panel gathers values only — it does not match rows in per-field mode. Wire matching through the new
RecordViewproponFilter(values)(Search and Clear both call it), typically a server query or your own client filter. The single-keyword box (and its built-in row matching) is unchanged when no field isfilterable.
v1.7
1.7.0
Added
- Configurable favicon via the runtime brand system.
BrandProvidergains afaviconUrlfield (alongsidelogoUrl): setNEXT_PUBLIC_FAVICON_URLfor a build-time default, returnfaviconUrlfrom yourNEXT_PUBLIC_BRAND_URLJSON (or calluseBrand().setBrand({ faviconUrl })) to change the browser-tab icon live per tenant, no rebuild. The staticapp/icon.*files remain the fallback.
v1.6
1.6.3
Fixed
RecordFormno longer loses in-progress input when switching tabs in dev. The draft persistence (persistKey) used a first-write flag that React StrictMode's effect double-invoke defeated: the second pass wrote the empty initial value over the draft the restore effect was about to bring back. The writer now skips the untouched seed by identity, so it never clobbers a stored draft. (Production builds, which don't run StrictMode's double-invoke, were unaffected.)
1.6.2
Fixed
- The scaffolded
next.config.tsnow pinsturbopack.rootto the app dir, so Next.js no longer infers the workspace root from a stray lockfile higher up the tree (a home-dirbun.lock, or an outer monorepo) and emits the "inferred your workspace root, but it may not be correct" warning.
1.6.1
Added
NEXT_PUBLIC_APP_URLsets the deploy origin (SITE.url), sometadataBase, canonical URLs, and Open Graph URLs resolve against your domain instead of the hard-coded demo host. Falls back to the demo URL when unset. Completes the env-driven brand set (APP_NAME/APP_TAGLINE/APP_DESCRIPTION/APP_URL).
1.6.0
Changed
- Documentation polish: package-manager commands in the docs now use tabs (npm / pnpm / yarn / bun), and the prose across the README,
AGENT.md, and the docs site was cleaned of em-dashes for a more natural read.
Added
- Runtime branding via `BrandProvider` / `useBrand()` (in the scaffold) — for white-label / multi-tenant apps that get their branding from an API. Env vars are the defaults; override them live from a
NEXT_PUBLIC_BRAND_URLJSON endpoint, a<BrandProvider initial={…}>seed, oruseBrand().setBrand(…). The name, tagline, description, logo, and the browser-tab title all update at runtime — no rebuild. Also routes the last hard-coded name spots (onboarding / register-business headers, the signin aside) through the runtime brand.
v1.5
1.5.4
Added
NEXT_PUBLIC_APP_NAMErenames the app — the sidebar, wordmark, auth screens, and browser-tab metadata read from one place (SITE.name) instead of a hard-coded "Vui Starter".NEXT_PUBLIC_APP_TAGLINEandNEXT_PUBLIC_APP_DESCRIPTIONcomplete the tab-title/meta rebrand.
Fixed
ChartContainerno longer triggers Recharts' "width(0) and height(0) … should be greater than 0" warning when it renders inside a hidden container (e.g. a kept-alive inactive tab). It waits for a measured size before mounting theResponsiveContainer.
1.5.3
Added
- The changelog and the [Upgrading](./AGENT.md#upgrading) guide now ship in the npm package. (They were authored for 1.5.2 but missed that tarball, which was published a moment before.)
1.5.2
Changed
- Turborepo guidance throughout.
init's monorepo note now walks you throughcd-ing into the target app (e.g.apps/web), installing deps in that app (or via a workspace filter), checking workspace globs, and running with a filter — and states clearly that turbo mode does not auto-install. - Fuller `AGENT.md`. Added an "Inside a Turborepo / monorepo" section and end-to-end walkthroughs for new-standalone, new-in-monorepo, and existing apps.
1.5.1
Changed
- Rewrote the README and
AGENT.mdprose to a clearer, more natural voice. No API changes.
1.5.0
Added
- `init` installs dependencies for you, using the package manager it detects from your lockfile (npm, pnpm, yarn, or bun). It prompts first;
--yesskips the prompt and--no-installopts out. This ends the "Module not found" cascade from runningdevbefore installing peers.
v1.4
1.4.3
Fixed
- The scaffolded
globals.cssno longer importstw-animate-css, so the theme is self-contained and needs no extra CSS dependency (VUI's own animations live intheme.css).
1.4.1
Added
- Turborepo support in `init` (
--turbo+--dir) — scaffolds into a target app directory. - A
tsconfig.jsonin the scaffold (@/*→./*) so the shell's imports resolve, and a TypeScriptnext.config.ts(so a fresh scaffold overwrites create-next-app's config instead of leaving two).
Fixed
- Scaffolding into a fresh Next.js app now works end to end.
- Pinned a patched
postcss(^8.5.12) via pnpm overrides to clear a high-severity advisory (GHSA-6g55-p6wh-862q).
1.4.1 shipped next.config.mjs and 1.4.2 was an interim republish; use **1.4.3
or later**, which ship next.config.ts and the self-contained theme.
1.4.0
Added
- `init` decision tree: fresh vs. existing project, and pre-built (shell + demo) vs. theme-only (
--fresh/--existing,--prebuilt/--theme-only).
v1.3
1.3.0
Added
- `RecordFormPanel` — the standard Add/Edit/View slide-over as a standalone export, for use outside a table (e.g. a Kanban board).
RecordFieldgains aninputtype (number/date) and renders aSelectforoptionsfields in the Add/Edit form.
v1.2
1.2.0
Added
- `npx @viliha/vui-ui init` — a scaffolder that copies the app shell (layout, sidebar, open tabs, command palette, nav config, logo) and demo pages into your repo. Ships a
binand a bundledtemplate/regenerated from the reference app on publish. - Navigation & open-tabs documentation.
v1.1
1.1.8
Fixed
- The slide-over record form auto-sizes its width to the content, so long field labels no longer wrap or overflow.
1.1.7
Baseline release.