All notable changes to this project will be documented in this file.
The format follows Keep a Changelog. This project adheres to Semantic Versioning.
- Column categories (core, Solid, React, Vue): opt-in
ColumnDefBase.category?: stringgroups columns for discoverability in the Columns/Sort/Group/Filter dropdowns' column lists instead of one long flat run of rows.groupColumnsByCategory/columnMatchesSearch/categorizedAlphabetizedByLabel(core) are the shared building blocks. Sort/Group render a category as a hover-intent flyout submenu (CategorySubmenu, portaled todocument.body/root to escape the panel's own scroll clipping); the Filter dropdown's left pane instead renders a category as an inline collapsible section (submenus aren't an option there —ArrowRightis already taken for left-pane→detail-pane crossing), collapsed by default except a category holding an active filter at the moment the dropdown opens. Each dropdown's search box now also matches a column's category name, not just its own label. See CLAUDE.md's "Column categories" section for full detail. - Columns dropdown redesign (Solid, React, Vue): replaces the single flat all-columns-with-a-checkbox list with a "Visible columns" section (every shown column, draggable/Alt+↑↓-reorderable) above an "Available columns" section (hidden columns, click to show; a categorized one collapses into a
CategorySubmenu). New core helpermoveVisibleColumnBy(Alt+↑/↓ within Visible skips over hidden columns, unlike the plainmoveColumnBy). New locale keyavailableColumnsSection(all 5 languages);columnsSection's text changed from "Display" to "Visible columns". - Delete/Backspace-to-remove + hover/focus background on active rows (Solid, React, Vue): a focused/hovered Sort active-sort/"Group order" row, Group active row, or Columns Visible row now gets a background tint (not just the native focus outline), and Delete/Backspace removes/hides it — the keyboard equivalent of clicking that row's own
×— matching the Filter dropdown's pre-existing equivalents. - Vanilla:
getProcessedData()onDataTableInstance— the current search+filter+sort row order (pre-grouping/pagination), mirroring React/Vue/Solid'sTableState.processedData, so a consumer no longer has to reach into@vates/data-table-core/internaldirectly (#22). - Vanilla:
clearAll()onDataTableInstance— resets search/filters/sort/group/page to true empty defaults, distinct fromsetViewState({})(which restoresinitialViewState's own defaults).
- Solid/React/Vue: a dropdown's non-draggable "Group order" row no longer shows the same hover highlight as a draggable row (it cancels just the hover cue, keeping the focus one, since the row is still clickable).
- Solid/Vue: the Filter dropdown's Escape-clear-search-term shortcut could clear the wrong column's value search — it checked only whether the currently selected column had a non-empty search term, not whether focus was actually inside the detail pane. Scoped to focus location. (React's equivalent handler is bound directly to that exact input, so it was never affected.)
- BREAKING (all four adapters):
defaultVisibleColumns/defaultPageSizeconstruction options replaced by a singleinitialViewState?: TableViewStateoption, covering every view concern (visible columns, column order, sort, filters, grouping, page/pageSize, search) uniformly.resetView/setViewState({})now restore this value instead of clearing to empty — closing a gap where a construction-time sort/filter/group default had no equivalent option and was silently lost on every reset (#20). Grouping a column viainitialViewState.groupBywith no matchingsortsentry auto-inserts one (mirroring what interactivegroup.togglealready does), unlessview.groupByis passed explicitly tosetViewState(which respects that view's ownsorts, or lack of one, as given).getViewState()'s own default-omission check now compares every field against what a reset would actually restore, instead of an independent "is this field non-empty" rule — this also fixes a latent bug wherevisibleColswas compared against every column rather than the real construction default.
- Grouping a column now auto-inserts a matching sort entry for it (ascending by default), so groups have a defined order right away instead of an arbitrary one (#17). The entry is an ordinary, user-removable/reversible sort — not tracked state — positioned after any earlier grouped columns' entries and before everything else, so multi-level nesting priority matches
groupBy's own order. Ungrouping does not remove it (a deliberate scope cut); reordering an active group re-syncs its sort priority to match.insertGroupSort/reorderGroupSorts(core, internal — wired automatically by each adapter'sgroup.toggle/group.move/group.moveByactions, same astoggleGroupBy) are the two new primitives behind this. - The Sort dropdown's active list now splits into a "Group order" section (entries matching a grouped column, in
groupBy's own order, not draggable — nesting there is never governed by drag position) above the pre-existing "Active sorts" section (now only non-group, tie-break entries, still fully draggable). Two new labels,groupOrderSection/groupOrderHint, added toDataTableLabelsand all five built-in locales. - The active bar merges a grouped-and-sorted column's sort chip and group chip into one pill (
[icon+label, toggles direction] [× removes sort] [⊞ opens Group dropdown] [× removes group]) instead of showing two identically-labeled chips for the same column. bucketLogRange(options?)/formatLogRange(options?, unit?, missingLabel?)(core, re-exported from every adapter) — a ready-madegroupValue/groupFormatpair for bucketing atype: 'number'column on a logarithmic scale, for a right-skewed column spanning several orders of magnitude (review counts, hours played, file sizes) where any single linearbucketNumericRangestep is either too coarse for the long tail or too fine for the low end.LogRangeOptions({ base?, divisions?, min? }) generalizes to a plain order-of-magnitude scale (base: 10, defaultdivisions: [1]), octaves/binary doublings (base: 2), a half-decade "1-3-10" grid (divisions: [1, 3]), or any other per-base-cycle split (#18)numericRangeGroup(step, unit?, missingLabel?)/datePartGroup(part, parseDate?, missingLabel?)/logRangeGroup(options?, unit?, missingLabel?)(core, re-exported from every adapter) — each bundles a bucketer with its matching formatter into one{ groupValue, groupFormat }pair from a single set of arguments, spreadable directly into a column def ({ key: 'hoursPlayed', ...logRangeGroup({ divisions: [1, 3] }) }), removing the config-divergence risk of passing the samestep/unit/part/optionsto both halves separately (#18)
- The toolbar's Group button now sits before Sort (was after) — grouping is the structural, primary concern and tie-break sorting is secondary, matching the Sort dropdown's/active bar's own group-before-sort ordering above.
groupData(core) now accumulates into aMapinstead of a plain object — a plainRecord's own keys silently enumerate any canonical non-negative integer string (anumericRangeGroupbucket key, or a raw integer groupBy value) in ascending numeric order regardless of insertion order, which made an unsorted numeric-bucketed group column look permanently pre-sorted for a reason unrelated tosorts/groupBy(#17).sortWithinGroups(core) now derives multi-level group nesting order fromgroupBy's own order instead of the relative order group-matching entries happened to have withinsorts— previously, reordering two grouped columns' sort entries via the Sort dropdown could nest groups in an order that visually contradicted what the Group dropdown displayed (#17).replaceSort(core) no longer duplicates a grouped column's own sort entry when that column is clicked via its header (reachable only throughkeepVisibleWhenGrouped, which keeps a grouped column's header clickable) — it now cycles that entry's direction in place instead (#17).bucketNumericRange/bucketDatePartnow returnnullfor a missing (null/undefined) value instead of silently coercing it —bucketNumericRangepreviously readNumber(null) === 0, merging "no value" into the same group as a real, confirmed0;bucketDatePartpreviously readString(null) === "null", surfacing the literal text"null"as a group header.bucketNumericRangealso now returnsnull(rather thanNaN) for a non-numeric value, sinceNaNpreviously flowed through to a group key that stringified to the literal visible text"NaN".formatNumericRange/formatDateParteach gained a 3rdmissingLabel = '(none)'parameter rendered for that group (#18)- Solid/Vue: the merged group+sort active-bar pill rendered as two visually disconnected chips instead of one seamless pill — the sort-remove ×'s border-square-off relied on a
:first-of-typeselector that never matched, since:first-of-typecounts by tag name and the pill's chip-body segment is itself a<button>and its actual first child. Replaced with an adjacent-sibling selector that targets the sort-remove × unambiguously. React was unaffected, since it applies the equivalent override as an inline style on that element directly rather than through a CSS selector.
- New package:
@vates/data-table-solid— the Solid.js implementation (createTableState/DataTableViewand all toolbar/dropdown components) that used to live entirely inside@vates/data-table-vanillais now its own standalone adapter package, for projects already using Solid.solid-jsis apeerDependencythere (never bundled), so it shares the consuming app's own Solid instance instead of a second, non-interoperable copy — Solid's reactivity tracking is module-scoped, so two separate bundled copies ofsolid-jswouldn't just cost bytes, a signal from one copy is invisible to a computation running in the other.@vates/data-table-vanilla's public API (createDataTable(container, options)) is unchanged; it's now a thin wrapper around@vates/data-table-solid, still bundling bothsolid-jsand@vates/data-table-solidinternally so non-Solid consumers never install either. Comes with its own demo app (demo/solid). @vates/data-table-solid: a<DataTable>convenience component, matching React's/Vue's own — builds acreateTableStateinternally and rendersDataTableView, for the common case that doesn't need view persistence or an imperative selection API.@vates/data-table-solid:createTableState'sdata/columnsparameters now also accept a SolidAccessor(mirroring Vue'suseTableState, which already acceptsMaybeRefOrGetter) — tracked reactively for the table's whole lifetime, with nocreateEffectto write by hand.<DataTable>is built on this.@vates/data-table-solid:usePersistedView/useUrlView/resetView/usePersistence— the view-persistence helpers React/Vue/vanilla already had, promoted from a hand-rolled equivalent that used to live only in the demo.- All adapters:
getRowId?: (row: TRow) => string | numberconstruction option — an opt-in escape hatch for row selection, which normally tracks rows by object identity (aSet<TRow>). WithgetRowIdset, selection matches and survives by id instead, so a refetch/re-map ofdatathat produces new-but-equivalent row objects no longer silently drops the current selection. Purely additive; behavior is unchanged for anyone who doesn't set it. Known limitation: shift-click range selection still matches its anchor/target by reference, so a data refresh between a plain click and a later shift-click can fall back to selecting just the target. - React/Vue/vanilla:
usePersistence/persistView— a combined persistence helper wiringlocalStorage+ URL sync from one{ storageKey?, paramName? }object instead of passing the same values separately to the storage/URL/reset helpers (previously a typo'd or forgotten key at one of those call sites was a silent bug). - Vue:
<DataTable>gainedv-model:pageandv-model:search-query, syncingtable.pagination.page/table.search.querytwo-way — Vue was the one adapter with nov-modelanywhere despite it being the idiomatic pattern for this. Nov-model:selection:selectionChange/onSelectionChangealready covers observing it, and "set it from outside" is an awkward fit for aSet<TRow>-by-object-identity model. - Vanilla:
onSelectionChange(cb): () => voidsubscribe/unsubscribe API onDataTableInstance, mirroringonViewChange— a listener can now be attached after construction, not just passed to the constructor. - Vanilla:
setRowKey/setSelectable/setOnRowClick/setLabels/setDefaultGroupsCollapsed/setGetRowIdpost-construction setters onDataTableInstance— all six of these options used to be frozen at construction time. - Solid/vanilla:
ColumnDef.renderFilterLabel?: (value: string) => Node— the Filter dropdown checklist can now render a custom node (e.g. a colored badge) per value, matching React'srenderFilterLabel/Vue's#filter-{key}slot. - Core:
countActiveSorts/countActiveGroups, alongside the existingcountActiveFilters, for a toolbar/active-bar badge count that reads the same way for all three concerns.
- BREAKING (React, Vue, Solid):
useTableState/createTableState's returnedTableStateis now namespaced by concern instead of one flat ~45-field object —table.columns,table.sort,table.filter,table.group,table.selection,table.pagination,table.search, each holding that area's own state/actions with the redundant prefix dropped (e.g.toggleSort→sort.toggle,moveGroupBy→group.moveBy).processedData/pagedData/groupedData/visibleItems/labels/getViewState/setViewState/clearAllstay top-level. SeeCLAUDE.md's "Namespaced TableState" section for the full field mapping. No compatibility shim — pre-1.0, so this ships as a hard cut.@vates/data-table-vanillais unaffected, since it always exposed its own distinctDataTableInstancemethod names rather thanTableStatedirectly. - BREAKING (Solid):
TableState.labelsis now anAccessor(call it astable.labels()), not a plain object, since it must itself react to a changedlabelsoption now thatlabels/defaultGroupsCollapsed/getRowIdare live options oncreateTableState(previously frozen at construction — the one adapter where they were, unlike React/Vue). - BREAKING (core):
calcTotalPagesrenamed tocomputeTotalPages, andselectedRowsOfrenamed togetSelectedRows— naming-consistency fixes (calc*was the only outlier amongcompute*-prefixed helpers;selectedRowsOfbroke the verb-first convention its siblings use). Pure renames, same signature and behavior. - BREAKING (core):
columnsis now a required parameter ofprocessData,groupData, andsortWithinGroups(previously optional, defaulting to[]) — for consistency with sibling functions that already required it, and because silently treating a missingcolumnsas "no columns known" was a footgun that dropped computed/value/groupValuecolumn support with no error. Every adapter already passescolumnsat every call site, so this only affects direct core callers relying on the old default. - Vue: internal state (
visibleCols,columnOrder,sorts,filters,excludeFilters,rangeFilters,groupBy,collapsedGroups) switched fromreftoshallowRef, matchingselection's existing usage — these are always replaced wholesale, never mutated in place, so the deep-reactive Proxy wrappingrefadded was pure overhead. No behavior change.
- All four adapters:
visibleColswas never reconciled against a replaced column set — Solid/vanilla viasetColumns/table.setColumns(the imperative "change the schema without rebuilding the table" API), React/Vue via thecolumnsargument itself changing across renders (no explicit setter needed there to hit it). Swapping to a fully disjoint set of column keys (e.g. switching to a different object type while reusing the same table instance/component) made every column filter out as "not visible" and the table silently render with none at all. A column that already existed keeps whatever visibility it had; a genuinely new column now starts visible by default. Fixed via a new shared core function,reconcileVisibleColumns, used identically by all three adapters that needed it (vanilla gets it for free through Solid). - Vanilla: the filter checklist/date-tree checkbox could silently fail to visually update on a real (trusted) mouse click, though the underlying filter state changed correctly — the click-revert-correction relied on a microtask, but a genuine trusted click's native checkbox revert can itself land after that microtask, undoing the correction
- Vanilla: the filter dropdown's left column pane could scroll a column button on top of its own sticky search box (missing
z-index, unlike the equivalent Sort/Group/Columns search row) - Vanilla: the filter dropdown's checklist/date-tree pane couldn't scroll — it had no bounded height to scroll within, so overflow content was silently clipped by the panel instead
- Vanilla:
render()'s own mounted subtree leaked a listener on everydestroy()—solid-js/web'srender()creates its own nestedcreateRootnever registered in the outer root's.owned, so the outerdispose()never reached it, leaving eachDropdown's document-levelclicklistener (and its closure over the disposed table's state) registered forever.destroy()now also disposesrender()'s own returned disposer. - React/Vue/vanilla:
usePersistedView/persistViewToLocalStoragealways wrote tolocalStorageeven once the view was back at its construction-time defaults, unlikeuseUrlView/syncViewToUrl, which already removes its query param in that case — in React specifically this meantresetView's ownlocalStorage.removeItemcould be immediately undone by the next save effect. Now matches the URL helpers' existing empty-view handling. - Vue:
DataTable/DataTableView's clickable-row styling (from self-detecting anonRowClicklistener) was frozen at whatever was true on first render — attaching or removing the listener later never updated it. Now re-derived after every re-render. - Vue: pagination rendered above the table instead of below, unlike React/Solid/vanilla.
- Fix various stale/incorrect documentation across every package's README and
CLAUDE.md/docs/*.md(missing exports, stale types, stale Solid/vanilla attribution left over from the Solid extraction, a broken example) — no code changes.
- Internal:
@vates/data-table-vanillais now implemented with Solid + TSX instead of a hand-rolledinnerHTML-string-rebuilding renderer.solid-jsis bundled (not a published dependency — never installed by consumers). The public API (createDataTable(container, options), its options, and its returned instance shape) is unchanged; bundle size grew from ~16.3 kB to ~24 kB gzip (still well under a React/Vue-based alternative's ~40 kB+), in exchange for structurally eliminating a class of bugs the old renderer's manual DOM-diffing bookkeeping was prone to (see Fixed below)
- Vanilla: the numeric/date range filter's min/max inputs reversed digit order while typing (e.g. typing "8" then "5" produced "58" instead of "85") — a
type="number"input doesn't support the selection APIs the old focus-restore mechanism relied on; fixed (ahead of the Solid rewrite) by switching totype="text" inputmode="decimal" - Vanilla: an array (multi-value) column's cell rendered via a bare comma-join ("Action,RPG") instead of ", "-joined, unlike React/Vue
- Vanilla: a custom
col.renderon an aggregate column was silently ignored (fell back toformat/stringify) —rendernow applies uniformly to data, group-header, and aggregate cells, matching React/Vue - Vanilla: Home/End keyboard navigation on table rows and group headers was entirely unimplemented
- Vanilla: Shift+ArrowUp/Down didn't extend row selection to the target the way Shift+click already did
- Vanilla: collapsing/expanding a group via Enter could drop keyboard focus to the page body
- Vanilla: opening one toolbar dropdown while another was already open could spuriously close the open one
- Vanilla: the header and per-group "select all" checkboxes could visually get stuck showing "checked" immediately after clearing a partial selection to zero
- Vanilla: shift-range deselection in the filter checklist/date-tree could clear an unrelated exclude flag on a value swept by the range
- Vanilla: the Filter dropdown's left column pane had no search box and wasn't alphabetized, unlike the Sort/Group dropdowns' equivalent lists
- Vanilla: the Filter dropdown's date-column detail pane was missing its select-all checkbox and value-search box entirely
- Vanilla: a date filter tree's month/day nodes rendered a raw zero-padded key ("05") instead of a localized month name ("May")
- Vanilla: the date range filter's min/max inputs didn't default to the column's actual data bounds, unlike the numeric range filter's equivalent inputs
- Columns/Sort/Group/Filter dropdowns gained a search box to narrow long column lists, roving Up/Down/Home/End keyboard navigation in visual order, Escape-to-clear-then-close, and focus-follows-open. The Filter dropdown's left column pane and right detail pane are now keyboard-navigable together (Left/Right crosses between them), and its column list behaves like a listbox (focus alone selects, no separate activation step)
- Active-bar chips (sort/group/filter) are now actionable, not just removable: a sort chip toggles direction in place, a group chip opens the Group dropdown focused on that entry, and a filter chip opens the Filter dropdown focused on that column
- Numeric/date range filter's min/max inputs now default to the column's actual data bounds instead of sitting blank, matching the adjacent slider's own default
- Exclude filters: a checklist value for a multi-value column (tags) can now mean "rows without this", not just "rows with this" — click cycles a value neutral → include → exclude → neutral, rendered as a native tri-state checkbox.
TableViewState.excludeFilterspersists/shares alongsidefilters ColumnDefBase.defaultSortDir— a column can opt into descending as the starting direction for a fresh sort (e.g. a "last modified" date or a score column), instead of always starting ascendingColumnDefBase.defaultValueSort— a column's filter checklist/date-tree can default to a sort order other than alpha-ascending (e.g. most-common-first for a tag column, most-recent-first for a date column's tree)- Vanilla:
DataTableInstance.getSelection()/setSelection()/clearSelection()give imperative access to selection, matching what React/Vue already exposed reactively viatable.selection/table.clearSelection()
calcTotalPages/paginateData(core) now guard against a non-finitepage/pageSize(e.g. an unvalidated input forwarded tosetPageSize), which previously collapsed the table to a silently empty page instead of falling back sanely; React/Vue'ssetPage/setPageSizealso now no-op on non-finite input rather than storing it@vates/data-table-core's main entry no longer re-exports vanilla-only theme internals (LIGHT_THEME/DARK_THEME/renderThemeCss) that had no relevance to React/Vue consumers — still reachable via the@vates/data-table-core/themesub-path, same pattern as/locales
- BREAKING:
Badge/ScoreBar(React/Vue) andcreateScoreBar/ScoreBarOptions(vanilla) are no longer exported from the published packages — they were demo-only presentational components with no theming/accessibility contract. Copy the component fromdemo/*/src/componentsif you were using it - BREAKING:
toggleFilter(core export, andTableState.toggleFilterin React/Vue) is removed — it was dead in every adapter's own UI code and unsafe to call directly (it never kept theexcludeFiltersinvariant in sync). UsecycleFilterValueinstead
- BREAKING:
useTableStatein@vates/data-table-reactnow takes a 3rdoptionsobject ({ defaultVisibleColumns?, labels?, defaultPageSize?, defaultGroupsCollapsed? }) instead of 4 trailing positional parameters, matching Vue's existing shape. ReplaceuseTableState(data, columns, visible, labels, pageSize, collapsed)withuseTableState(data, columns, { defaultVisibleColumns: visible, labels, defaultPageSize: pageSize, defaultGroupsCollapsed: collapsed }) - BREAKING: core's
setSortexport (andTableState.setSortin React/Vue) is renamedreplaceSortfor clarity, sincesetSort/toggleSort/appendOrToggleSortsounded like near-synonyms despite very different effects. Same signature and behavior, pure rename rowKey(React/Vue/vanilla) is now documented as a rendering-identity hint only (Reactkey/Vue:key/vanilla DOM key), not a selection identifier — selection has always been tracked by object identity regardless ofrowKey. Docs only, no behavior change
ColumnDefBase.compare— a custom(a, b, dir) => numbercomparator for a column whose natural order is neither numeric nor alphabetical (e.g. an enum/tier column). Applied everywhere a column's values are ordered: row sort, group order (for a groupBy column), and the filter checklist's default and explicit (sortFilterValues) ordering. The 3rddirargument only matters for a value that must stay pinned to one end regardless of direction (impossible to express as a plain return value, since that gets sign-flipped fordescthe same way the default comparison does) — ignore it for an ordinary comparator (#15)compareMissingLast(compare?, isMissing?)(core, re-exported from all three adapters) — a ready-madecomparebuilt on thedirargument above, pinning a value (missing data, by default) last regardless of sort direction (#15)
ColumnDefBase.groupValue/groupFormatlet a column bucket into a coarser group key instead of grouping on its exact value — useful for a continuous or high-cardinality column (a percentage, price, or raw timestamp) where exact-value grouping puts every row in its own group.bucketNumericRange/formatNumericRangeandbucketDatePart/formatDatePart(core, re-exported from all three adapters) are ready-made pairs for numeric-range and date-part bucketing- Numeric and date range filters gained a slider (two overlapping thumbs sharing one track) alongside their existing min/max inputs. For date columns, the range now also narrows the Year › Month › Day tree itself (and its facet counts), the same way the search box already does, instead of only being ANDed onto the final row set
- The filter checklist/date-tree now fills the detail pane's actual height instead of a hardcoded 260px, which used to leave dead space below a short checklist (vanilla) or let the date tree overflow past the panel onto the page entirely (all three adapters) whenever the column list was taller than the old default (#13, #14)
- The filter column list's active-filter dot only checked
rangeFiltersfortype: 'number', so a date column's own range filter never lit it; unified to check the checklist size OR the range bounds for every column type - The active state bar never rendered a chip for a range filter at all; it now does, and the chip's
×clears the range too
- Sort and Group toolbar dropdowns redesigned: an "Active" section lists current entries, each draggable or reorderable with Alt+ArrowUp/Alt+ArrowDown; a Sort entry's direction toggles by clicking (or Enter/Space) the row itself, and a single
×removes it. An "add" section below lists the remaining columns as real, keyboard-reachable buttons - Columns panel rows get the same drag-and-drop + Alt+ArrowUp/Alt+ArrowDown reordering, replacing the old ▲▼ buttons
- The search input gained an inline
×button that clears just the query and returns focus to the input moveSortBy/reorderSortcore primitives, andreorderColumn/reorderSortgained anafterparam for drag-and-drop reordering- Toolbar reorganized into two clusters — Columns/Sort/Group ("shape" the view) and Search/Filter ("narrow" it) — separated by a divider, with "Clear all" pinned alone at the far right
- An always-visible active state bar below the toolbar shows one removable chip per active sort entry, group column, and filter column, plus the row/group-count stats — replacing the old bare count badges on the Sort/Filter/Group toolbar buttons
- Each Sort/Group/Filter toolbar button grew an adjoining
×button (shown only when that state is non-empty) that clears it without opening the dropdown, replacing the dropdowns' old in-panel "Clear sorts"/"Clear groups"/"Clear filters" footer row
- Dropdown panels (Columns/Sort/Filter/Group) now clamp themselves to stay within the viewport instead of rendering partly or fully off-screen near an edge
- Dragging a row to the very end of the Sort/Group/Columns dropdown lists now works — previously a drop past the last row, or onto its bottom half, was silently rejected or could only ever insert before it
- Removed a redundant "× " prefix baked into the
clearSorts/clearGroups/clearFilterslabels, left over from when they were visible footer text rather than tooltip/aria-label-only text next to a button that already shows its own×glyph
ColumnDefBase.searchableflag (defaults totrue) excludes a column from global search — useful for a column whose underlying value isn't user-facing text (e.g. an image URL rendered viarender)
- Vanilla: table scroll position no longer resets to the top on every re-render (sort, filter, page change,
setData) — preserved the same way focus already was - Sorting and grouping by the same multi-value column (e.g. tags) now sorts correctly: a secondary sort key applies within each group instead of being starved by an incidental whole-array comparison, and the groups themselves are ordered by their own value instead of an arbitrary order
- Full keyboard navigation for table rows and group headers: a roving tabindex moves one row (or group header) at a time with arrow keys, crosses page boundaries at the edges, Home/End jump to the current page's ends (Ctrl/Cmd+Home/End jump across all pages), Space toggles selection (Shift+Arrow/Home/End extends the range), and Enter fires row click or toggles a group's collapsed state
defaultGroupsCollapsedoption (defaults totrue) — groups now start collapsed unless overriddenColumnDefBase.parseDateoverride fortype: 'date'columns, so a column can plug in its own date parser whennew Date(v)guesses an ambiguous format wrong- Filter checklist virtualized (windowed rendering) — columns with thousands of distinct values no longer mount one row per value regardless of scroll position
resetViewhelper (React/Vue/vanilla) puts a table back to its construction-time defaults and clears any persisted localStorage/URL state in one call
type: 'date'and numeric-stringtype: 'number'columns now sort chronologically/numerically instead of falling back to alphabetical string comparison- Pagination counts group header rows toward
pageSize, so a page never renders more rows than configured; the "Rows per page" dropdown now shows the correct value even whenpageSizeisn't one of the hardcoded options - Vanilla: an open toolbar dropdown no longer stays visibly open after clicking a row
- Visual hierarchy improved between the table header, group header rows, and the odd-row stripe (header gets a stronger anchor, group rows read as bold section dividers, stripes are more subtle)
- Global search and the filter checklist search are now diacritic-insensitive (e.g. "ooo" matches "Öoo")
- Select-all checkboxes (header, group headers, filter checklist) now clear the selection when clicked in an indeterminate or fully-checked state, and only select all when nothing is selected — matching Gmail's convention instead of always escalating to select-all
- Filter facet-count computation scoped to only the currently open column (was every filterable column on every change) — ~15-17x faster at 500k rows/7 columns; date-column sort now precomputes comparable values once instead of per comparison — ~4x faster
- Added a Vitest benchmark suite (
npm run bench -w packages/core) for core logic at 10k/100k/500k rows - Demo apps: added a 200k-row "Huge dataset" showcase section (realistic e-commerce data, two groupable columns), extended view persistence/sharing to every table, fixed a dark-mode button contrast bug and a GitHub Pages duplicate-React bundling bug
- Bumped CI actions to v5 and the Vite toolchain to fix an esbuild vulnerability
- Filter dropdown redesigned as a searchable master-detail panel — left pane lists all filterable columns, right pane shows the selected column's controls — replacing the old single stacked checklist that didn't scale to high-cardinality columns
- Per-value row counts shown in filter checklists, computed as facets over every other active filter
- Year › Month › Day filter tree for date columns, replacing a min/max range
- Filter checklist value sorting (alphabetical/count, ascending/descending)
- Column reordering via drag-and-drop on headers and ▲▼ buttons in the Columns panel
- Shift-click range selection for table rows, filter checklist values, and the date filter tree — extends or shrinks the selection between the last-clicked item and the shift-clicked one
DataTableView(React/Vue): render layer split out fromDataTableso external code (e.g.usePersistedView/useUrlView, or a fully custom layout) can access the underlyinguseTableStatevalue directly
- Dark theme contrast improved across the vanilla, React, and Vue adapters
- Vue: aggregate row no longer renders when no grouping is active
- Theme palette single-sourced in core and shared by vanilla, React, and Vue
- npm publish switched to trusted publishing (OIDC)
- Demo docs cross-linked and reorganized (sticky nav, Row click/Aggregation docs, locale switcher fix)
@vates/data-table-core— framework-agnostic pure-TypeScript logic: sorting, filtering, grouping, pagination, aggregation, global search, column visibility, i18n@vates/data-table-react— React adapter with<DataTable>component anduseTableStatehook; render props for custom cells and filter labels@vates/data-table-vue— Vue 3 adapter with<DataTable>component anduseTableStatecomposable; scoped slots for custom cells, filter labels, and group headers@vates/data-table-vanilla— framework-free adapter (createDataTable) for use without React or Vue- i18n via a
labelsprop/option (defaults to English); built-in locales for English, French, Spanish, German, and Portuguese, all overridable - Client-side pagination (
defaultPageSize,page,pageSize,numPages,setPage,setPageSize) - Row selection: opt-in checkbox column with header and group select-all (including indeterminate state),
onSelectionChange/selectionChange/selectionfor headless usage - Row click callback (
onRowClick/rowClick) with hover highlighting, skipping group headers, the aggregate row, and the selection checkbox - Global text search across all columns
- Group header aggregation (
sum,count,avg,min,max, or a custom function) rendered in a secondary row below each group header - Array-valued (multi) columns for filtering, grouping, and display
- Computed columns via a
valueaccessor function, decoupling a column's cell value from itskey - Grouped columns automatically hide from the table header and reappear when grouping is cleared
- View persistence and sharing:
getViewState/setViewStatesnapshot sort/filter/group/page/search state;usePersistedView/persistViewToLocalStorageanduseUrlView/syncViewToUrlhelpers for localStorage and shareable-URL persistence