npm

Community

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

Latest
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.com host 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_block and compose_page read @viliha/vui-web wherever it resolves, a sibling package in a monorepo or node_modules in 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-aurora for a brand wash behind a hero, .vui-grid-bg and .vui-dot-bg for a faint texture that fades out downward, .vui-glow for a spotlight, .vui-gradient-text for a gradient headline, .vui-lift for a card that answers the pointer, .vui-ring-gradient for a hairline brand edge, and .vui-reveal for 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 a gradient hero 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 Avatar and AvatarFallback and 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 new src gets 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 and fit="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.css gains the appearance tokens behind data-sidebar, data-density and dir, 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/laravel and /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 @props and $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.ts reads source text, so moving a class into class-variants.ts would have left it checking a file that no longer mentions z-[200] or bg-popover and 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.ts so 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. tabsListVariants is now a plain function rather than a cva instance; it takes the same { variant } argument and returns the same string.
Added
  • Charts that are not React-only: `TanStackChart`. ChartContainer and 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 currentColor and reads six CSS variables, so one class in theme.css is 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.css for anyone running Tailwind v4, and a compiled vui.css for people with no build step at all. Both are generated from packages/ui/src/theme.css at 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-icon now gets the same treatment. The accordion animation reads --vui-accordion-height instead of Radix's variable, and AccordionContent maps 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.json said MIT but no LICENSE file shipped with the package, so anyone auditing node_modules found 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.json and 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-table was in the CLI's dependency list, so every scaffolded app pulled it in. No file in the package has ever imported it: the datatable is RecordView, 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. postcss in the lockfile was pinned to 8.5.19, which carried nanoid 3.3.15 and two high-severity advisories (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8). Refreshing the lockfile moves postcss to 8.5.26, which requires the patched nanoid. 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 equivalent mcpServers entry in Cursor or any other MCP client) and it gets seven tools:
  • list_guides and get_guide serve 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_components lists every export with its import specifier, and get_component returns one component's source or, when the file is large, its props and exported types plus the docs about it.
  • list_pages and get_page serve the reference app's pages, layouts and shell components with their routes, so an agent copies a working page instead of inventing one.
  • search_docs searches 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-primary bubble 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 uses bg-popover with 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 use bg-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 IoAction type 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, file for an import, applyRows to put records into the table, refetch to reload from the server, and `query`, the active page, sort, search and filters. That last one matters: in fetcher mode 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, with accept filtering 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 fetcher mode a cache hit returned early and no request was ever made, and the default TTL was 0, 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=0 there 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 RecordView turns 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. Dropdown rendered in place at z-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. AlertDialog was at z-50, under the z-60 slide-over, so a confirm raised from a form was unreachable; it is at z-80. Popover moved to the picker layer, HoverCard above 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. formColumns on 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`. sectionColumns flows the cards across the form; sections declares 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. Omit sections and they still come from each field's group, 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 description now 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 fetcher table 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, showPlan and showAdd, resolved through VuiProvider like everything else, so an app with no billing turns off the plan line once rather than per screen.
  • Switching organization repaints the app. Each Organization can carry a theme, handed to ThemeConfigProvider as 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) and WorkspaceProvider, with "Add organization" going to the existing /register-business wizard.

v1.56

1.56.1

Fixed
  • Every font in the picker is now actually loaded. FONT_FAMILIES offered 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 with next/font and their variables are on <html>, so all four options change the type.
  • A family that isn't loaded says so. ThemeConfigProvider checks 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_FAMILIES documents 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, /terms and /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 SiteFooter gets 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_FIELDS is 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. --brand is the single value a theme sets; the hover state, focus ring, selection colour and button shadow derive from it in theme.css with color-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.tsx with ORG_THEME from lib/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-4 content 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, ignoring editable, so a read-only choice column could be changed for every selected row at once. It now needs editable as 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 it editable: true to get it back.
  • A per-table `behaviour` prop now reaches the form, not just the rows. confirmDiscardWhenDirty and closeOnSave set on one RecordView were 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. after places one under a named field, in that field's own section; group names a section and puts it at the end. render receives 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 renderInput that 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, 0 turns it off), confirmDelete, and confirmDiscardWhenDirty (ask before throwing away unsaved edits, off by default because that is how Cancel has always behaved). The shipped values live in vuiPreset, 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() returns preferences, userConfigurable, setPreference and reset for 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. vuiPreset is 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. Ships VuiProvider, useVuiConfig, defineConfig and mergeConfig. 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 takes label, variant, icon, align (start pins it left, where destructive actions belong), confirm, and visible / disabled predicates reading the live form. Its onAct gets mode, row (the draft), dirty, valid, errors, close, reset and edit, 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 fetcher mode 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. onDataChange may now return a promise, and RecordView waits for it before reloading. If it rejects, the error goes to onError and 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. In manual mode (where the host owns the fetch) returning a promise also re-emits the current query through onQueryChange once 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 Combobox or Select whose 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. MultiCombobox chips 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 multiple fields, 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 only resolveOption, 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. Unlike render it 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 + resolveOption used to render the stored value first, so a page opened as Region 1 · Country 1 · Timezone 27 and 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 resolveOption fails or returns nothing, the cell said Currency 142 forever with no sign anything had gone wrong. It now reads Unknown with the id in the tooltip, so a dangling foreign key looks like an error and stays debuggable. Applies to multiple fields 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 / showSort and the rest. It's the one that doesn't default to true: left unset it follows the fields, as above. Pass showEdit={false} to drop Edit from a table that does have editable fields, or showEdit to force it on. onEdit still 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 drive value yourself). 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`. meta prints a details line under the preview (name · format · dimensions · size · date; dimensions are read off the rendered image when you don't supply them). accept now defaults to every image type instead of a fixed PNG/JPEG/SVG list. maxBytes rejects an oversized file before onPick is called. busy lets 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 neither onPick nor inline says so instead of quietly producing a value that cannot be saved. organizationProfileFields (the prebuilt demo preset) keeps inline, 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 a string[], renders the selection as removable chips, and picking an option toggles it while the popover stays open. Static options or an async source (fetch on open, debounced search, and batch label-resolve of the selected values).
  • Multi-select `RecordField` (many-to-many form fields). Set multiple: true on a choice field and its value becomes a string[]: the Add/Edit form renders the multi-select with chips, and the read cell shows up to maxChipsInCell (default 3) labels then "+N" with the full list in a tooltip. Async fields add a batch resolveOptions(values) (the plural companion to resolveOption). required means at least one selected (blocks Save). Reuses the existing loadOptions / dependsOn contract; nothing loads on mount. Demo: Markets → Post Codes.
  • `AsyncOptionSource.resolveOptions`useAsyncOptions now batch-resolves set values in one call when a source provides it (falls back to per-value resolveOption).

v1.43

1.43.0

Added
  • Trash mode — view soft-deleted rows and restore them. Set showTrash to 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 (trashedData in client mode, or the new trash: true flag on ServerQuery for manual/fetcher mode) 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 from trashedData (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 a render (the read view — e.g. a status badge in the table) with input:"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 RecordField now takes declarative rules: min/max (character length, or numeric value for input:"number"), pattern (regex + patternMessage), format: "email" | "phone", a custom validate(value, draft), and trim. 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-7890 while typing. Previously the form enforced only required (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 RecordField with loadOptions/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 via resolveOption (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-options fields 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 native to make every password field use the browser's bullet-dot type="password" (so password managers and autofill work), or leave it asterisk (default) for the * overlay. The per-field mask prop still overrides it. Declare the var in turbo.json globalEnv if you read NEXT_PUBLIC_* inside packages/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 it fields (grouped into sections via each field's group) and data, 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), the OrgProfile type, getOrgPrimary, ORGANIZATION_PROFILE_DESCRIPTION, and the reusable BrandAsset logo/favicon control. Spread or override the fields to fit your schema. The backoffice /organization/profile page is now a thin consumer of both.

v1.39

1.39.0

Added
  • Form sections take any title. A RecordField's group accepts any string now, not just the four built-in names, and RecordForm renders 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) and renderInput (edit control), RecordForm now shows render while viewing and renderInput while editing — so a cell like a logo preview or a status badge stays editable in Edit mode. Before, render always 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's resizableColumns now defaults to the NEXT_PUBLIC_RESIZABLE_COLUMNS env var, which is on unless you set it to 0 or false. Drag a column's right edge to widen it, so a long value in a narrow column is always reachable. Pass resizableColumns={false} on a single view to opt out. Declare the new var in turbo.json globalEnv if you read NEXT_PUBLIC_* inside packages/ui.
Fixed
  • Custom-rendered cells no longer overflow into the next column. A cell with a render function (for example a long status badge like PARTIALLY_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's width.

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 controlled data prop), 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 controlled data path 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 real type="password" (bullet dots) that the eye toggle flips to type="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 for Input inside a Field: spread bind(...) and pass error for 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 run helper so bind and validate satisfy react-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 text Input / Textarea fields on blur and on submit, surfacing every error through a single channel: the field's own inline error. Spread bind(key) onto the control, read errors[key], call validate() on submit, and set noValidate on the <form> so the browser's native bubble can't compete. Rules receive all field values for cross-field checks (e.g. confirm-password). Returns values, 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 for Input and Textarea (multiline puts the icon at the top). The reference app's auth Field now re-exports this one.
Fixed
  • Auth screens: no more double validation. Sign in / up and forgot / reset password set noValidate and validate through useFormFields, 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 was minmax(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 plain max-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.json description around real search terms (React component library, admin dashboard template, Next.js, Tailwind CSS v4, shadcn/ui, datatable), expanded keywords, 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. Wizard renders the stepper (from steps + current), a scrolling body for the active step, and a Back/Next footer; WizardSection is 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 to max-content (no dead space) and aligned across rows. Supports required and hint. 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 Combobox per level, where picking a level narrows the next from the selected node's children and clears everything downstream. A level is disabled until its parent is chosen. Data-driven — pass a CascadeNode[] 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 + FilterField render 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 FilterField rows into the Filter panel (below the filterable fields, in the same grid). The panel now opens when there are filterable fields or filterExtras.
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 from filterable config or filterExtras. Panel widened to fit.

v1.31

1.31.0

Added
  • `@viliha/vui-ui/auth-context` — a provider-agnostic auth contract. Exports AuthProvider, useAuth(), and the AuthContract / AuthUser / Credentials / SignUpInput types. 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 no AuthProvider is 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 to useAuth(); the app mounts a Better Auth adapter (app/_components/auth-provider.tsx) that activates when NEXT_PUBLIC_AUTH_BASE_URL is 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: command ships the cmdk primitives without CommandDialog (VUI's command-palette is the modal ⌘K menu); sonner's Toaster reads the .dark class directly instead of next-themes; calendar uses VUI's Button/buttonVariants; theme.css already 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.css now ships the caret-blink keyframe so the OTP caret animates without tw-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 existing radix-ui / class-variance-authority / lucide-react). AlertDialogAction / AlertDialogCancel style the Radix element with VUI's buttonVariants (VUI's Button has no asChild), so they still accept Button variant/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 running shadcn add: accordion, label, separator, skeleton, switch, tabs. New runtime deps: radix-ui, class-variance-authority, lucide-react. theme.css now ships the accordion-down/accordion-up keyframes so the Accordion animates without tw-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-column FieldGrid, 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 readme field, 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 manifest readme injection to publish.mjs; it had no effect and was removed the same day — the per-version readme field 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, and showSelection. 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 readme field in the publish request, which pnpm publish (and yarn-classic / bun) do not send — only npm publish does. Publishing now goes through scripts/publish.mjs (run it with pnpm release or node scripts/publish.mjs, from any package manager): it strips the internal workspace:* devDeps to keep the manifest spec-valid, then uploads via npm publish, so the README shows again. A prepublishOnly guard blocks a bare <pm> publish and points at the script.

v1.25

1.25.2

Added
  • `Input` invalid state — an <Input> with aria-invalid now paints its border and focus ring in the destructive color, 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 !important is needed.
Fixed
  • `Toaster` hydration mismatch — the portal was gated on typeof document === "undefined", which is false during 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's refetch when 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's syncOrganizations(since)) revalidates with a delta (?since=cursor → only changed rows + deleted ids, merged by id), not a full reload. Documented in AGENT.md (Open tabs) and docs /layout.

1.25.0

Added
  • Async option loading for pickers (`loadOptions` / `resolveOption`) — a reusable primitive so Combobox, Select, and RecordView choice/filter fields fetch their options on demand instead of receiving a fully-loaded static options array. 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-record resolveOption (never the whole list). Powered by the exported useAsyncOptions hook.
  • `Combobox` / `Select` accept a source ({ loadOptions, resolveOption }) in place of options, plus resetKey for 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), and dependsOn — so form and filter comboboxes lazy-load, with values giving the current draft / filter values and dependsOn clearing the cache + selection when a cascade parent changes. Works where onFormOpen (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 (mode is "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 — unlike onCreate / onView / onEdit (which redirect and suppress the panel), onFormOpen does not suppress anything. In panel mode row is 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 by npm publish). pnpm publish rewrites those to spec-valid versions, so a prepublishOnly guard now blocks any non-pnpm publish. Run cd 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.md and npmjs renders from the publish payload, which pnpm publish leaves empty (the registry's readme field was frozen at a stale, older copy while the tarball was current). Publish this version with `npm publish` from `packages/ui` (not pnpm 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. RecordView now reads NEXT_PUBLIC_DEFAULT_PAGE_SIZE (initial rows per page) and NEXT_PUBLIC_MAX_PAGE_SIZE (the ceiling the page-size selector won't exceed), both overridable per table via the new `defaultPageSize` / `maxPageSize` props. In fetcher/manual mode 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.ts returns one page at a time (search / sort / filter / paginate on the server, page size clamped to the max), and the view wires it with RecordView's fetcher. 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 organizations page and documented in AGENT.md and the docs site. Data access lives in lib/api/<entity>.ts (async, no React — the real-API seam), a controller hook use-<entity>.ts owns { data, loading, error } and loads *after mount*, and the presentation renders RecordView with no data processing of its own.
  • UI-first navigation. The presentation now paints its shell first: a thin *-table.tsx next/dynamic-loads the datatable view behind a TableSkeleton, and the controller starts loading: true with 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 publish packs README.md into the tarball but doesn't set the registry readme field the website renders (it was empty for 1.21.0/1.21.1). Publish the package with `npm publish` (from packages/ui), not pnpm publish. No code change.

1.21.1

Fixed
  • List README.md in the package files so 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, and success/error/warning variants). It's a module store, so toast(...) works from anywhere — event handlers, catch blocks — with no provider; mount <Toaster /> once in the root layout (the scaffold now does). Dependency-free (portal + fixed positioning, like Tooltip), 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 by nameSortKey, or auto-detects the first hideInTable field marked sortable (the one driving getPrimary). No opt-in beyond marking that field sortable; if none is sortable, the header stays static (unchanged).

v1.18

1.18.0

Changed
  • `Tooltip` now matches the shadcn design (dark bg-primary bubble + 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 a crumbs prop that fully replaces the default Home › {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. The Crumb type 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 options now renders its matching label in the table (e.g. SYSTEM → "System") while staying editable — no need for a read-only render just 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 maxCellChars is 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 to NEXT_PUBLIC_MAX_CELL_CHARS (or 25) and is overridable per view (maxCellChars prop) or per column (a field's maxChars; 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, manages data / rowCount / loading, aborts superseded requests via the signal, and (with cacheKey) caches responses so returning to a tab is instant with no refetch. Combine with persistKey to 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-wired data/onQueryChange/loading + Map boilerplate (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 Map keyed by the query (served synchronously, no shimmer) and passes persistKey, 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's KeepAliveTabs was overwriting a route's cached element with Next's fresh children on 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 own useSearchParams.

1.13.0

Added
  • Server-side ("manual") mode for `RecordView`. Set manual and RecordView stops filtering/sorting/paginating data — it renders it as the current page verbatim and reports the query via onQueryChange({ page, pageSize, sort, search, filters }) so your backend does the work. Pair with rowCount (drives the footer + page count) and loading. 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-shimmer sweep in theme.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; clear loading when 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&apos;s options can now depend on another field&apos;s current value, in both the Add/Edit form and the Filter panel. Opt-in, backward compatible.
  • Form: RecordField.options may be a function of the draft((draft) => { value, label }[]) — recomputed as the draft changes.
  • Filter: FieldFilter.options may be a function of the current filter values((values) => { value, label }[]). FieldFilter is now generic (FieldFilter<T>).
  • Auto-clear: when the parent changes and the child&apos;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 as Select (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 an options field for a searchable control (default stays Select). 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: true sorts a field with no column (e.g. a hideInTable name shown via getPrimary) — it appears in the Sort dropdown.
  • sortable: false keeps 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 filterable and 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). options falls back to the field's own options.
  • 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 RecordView prop onFilter(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 is filterable.

v1.7

1.7.0

Added
  • Configurable favicon via the runtime brand system. BrandProvider gains a faviconUrl field (alongside logoUrl): set NEXT_PUBLIC_FAVICON_URL for a build-time default, return faviconUrl from your NEXT_PUBLIC_BRAND_URL JSON (or call useBrand().setBrand({ faviconUrl })) to change the browser-tab icon live per tenant, no rebuild. The static app/icon.* files remain the fallback.

v1.6

1.6.3

Fixed
  • RecordForm no 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.ts now pins turbopack.root to the app dir, so Next.js no longer infers the workspace root from a stray lockfile higher up the tree (a home-dir bun.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_URL sets the deploy origin (SITE.url), so metadataBase, 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_URL JSON endpoint, a <BrandProvider initial={…}> seed, or useBrand().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_NAME renames 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_TAGLINE and NEXT_PUBLIC_APP_DESCRIPTION complete the tab-title/meta rebrand.
Fixed
  • ChartContainer no 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 the ResponsiveContainer.

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 through cd-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.md prose 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; --yes skips the prompt and --no-install opts out. This ends the "Module not found" cascade from running dev before installing peers.

v1.4

1.4.3

Fixed
  • The scaffolded globals.css no longer imports tw-animate-css, so the theme is self-contained and needs no extra CSS dependency (VUI's own animations live in theme.css).

1.4.1

Added
  • Turborepo support in `init` (--turbo + --dir) — scaffolds into a target app directory.
  • A tsconfig.json in the scaffold (@/*./*) so the shell's imports resolve, and a TypeScript next.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).
  • RecordField gains an input type (number/date) and renders a Select for options fields 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 bin and a bundled template/ 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.