@viliha/vui-ui 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT.md CHANGED
@@ -334,6 +334,8 @@ Build forms with VUI and shadcn/ui together, reaching for shadcn Form, React Hoo
334
334
 
335
335
  Never hand-build an HTML table; always use `RecordView`. Configure columns through its field props (`editable`, `required`, `copyable`, `options`, `render`), and let `RecordView` own the rest: sorting, filtering, pagination, bulk actions, and import/export.
336
336
 
337
+ Sorting follows column visibility by default; use `sortable` to decouple them — `sortable: true` sorts a field with no column (e.g. a `hideInTable` name shown via `getPrimary`), `sortable: false` locks a visible column. Set the flag; don't rewire the Sort dropdown or headers.
338
+
337
339
  ## Filtering
338
340
 
339
341
  The Filter panel is a single keyword box by default (built in, matches all fields). For a labeled control per field, set `filterable` on the relevant fields and never hand-roll a filter form: `filterable: true` (text) or `filterable: { control, label, placeholder, options }` where `control` is `"text" | "number" | "date" | "select" | "combobox" | "checkbox"`. When any field is filterable the panel shows a control per field plus Search / Clear. It only **collects** values — handle `onFilter(values)` on `RecordView` to run a query or client filter (Search and Clear both call it). Types: `FilterControl`, `FieldFilter`, `FilterValues<T>`. Need a control kind that isn't listed? Extend the `FilterControl` union in the component.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ backward-compatible features, **major** for breaking changes.
7
7
 
8
8
  To upgrade, see [Upgrading](./AGENT.md#upgrading) in the agent guide.
9
9
 
10
+ ## 1.9.0 — 2026-07-26
11
+
12
+ ### Added
13
+
14
+ - **`sortable` field flag in `RecordView`** — decouples sorting from column
15
+ visibility. By default a field is sortable iff it's a visible column
16
+ (`!hideInTable`), unchanged. Now:
17
+ - `sortable: true` sorts a field with **no column** (e.g. a `hideInTable` name
18
+ shown via `getPrimary`) — it appears in the Sort dropdown.
19
+ - `sortable: false` keeps a **visible column unsortable** (its header stops
20
+ being a sort toggle).
21
+
22
+ Affects both the Sort dropdown and the column-header click. Fully backward
23
+ compatible — omit the flag for today's behavior.
24
+
25
+ **For agents:** to make a non-column field sortable (or lock a column), set
26
+ `sortable` on the `RecordField`; don't rewire the Sort dropdown or headers.
27
+ Demo: `system/regions` sorts by Name (which has no column).
28
+
10
29
  ## 1.8.0 — 2026-07-26
11
30
 
12
31
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viliha/vui-ui",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Vui UI — a clean, token-driven React admin/CRM component library built on Tailwind CSS v4, shadcn-style patterns, and Radix Icons. Ships as TypeScript source (Just-in-Time), compiled by the consuming app.",
5
5
  "license": "MIT",
6
6
  "author": "Suman Bonakurthi",
@@ -226,6 +226,12 @@ export interface RecordField<T> {
226
226
  copyable?: boolean;
227
227
  /** Show in the detail panel only, not as a table column (e.g. first/last name). */
228
228
  hideInTable?: boolean;
229
+ /** Whether this field can be sorted — decoupled from column visibility.
230
+ * Default: sortable iff it's a visible column (`!hideInTable`), the historic
231
+ * behavior. Set `true` to sort a field with no column (e.g. a `hideInTable`
232
+ * name shown via `getPrimary`); set `false` to keep a visible column
233
+ * unsortable. Controls both the Sort dropdown and the column-header toggle. */
234
+ sortable?: boolean;
229
235
  /** Custom, non-editable cell/value renderer. */
230
236
  render?: (row: T) => React.ReactNode;
231
237
  /** If set, the field becomes a choice field: the Add/Edit form renders a
@@ -460,6 +466,12 @@ export function RecordView<T extends { id: RowId }>({
460
466
 
461
467
  const tableFields = fields.filter((f) => !f.hideInTable);
462
468
  const visibleFields = tableFields.filter((f) => !hidden.has(f.key));
469
+ // Sorting is decoupled from column visibility: a field is sortable when its
470
+ // `sortable` flag says so, else it falls back to "is a visible column".
471
+ const canSort = (f: RecordField<T>) => f.sortable ?? !f.hideInTable;
472
+ // Fields offered in the Sort dropdown (may include hidden-but-sortable fields
473
+ // and exclude visible-but-unsortable ones).
474
+ const sortFields = fields.filter(canSort);
463
475
  // Fields opted into per-field filtering. Non-empty → the Filter panel renders
464
476
  // a control per field instead of the single keyword box.
465
477
  const filterFields = fields.filter((f) => f.filterable);
@@ -1182,7 +1194,7 @@ export function RecordView<T extends { id: RowId }>({
1182
1194
 
1183
1195
  <Dropdown label="Sort" icon={<ArrowUpDown className="size-3.5 text-blue-500" />}>
1184
1196
  <DropdownLabel>Sort by</DropdownLabel>
1185
- {tableFields.map((f) => (
1197
+ {sortFields.map((f) => (
1186
1198
  <DropdownItem
1187
1199
  key={f.key}
1188
1200
  onSelect={() => toggleSort(f.key)}
@@ -1300,32 +1312,44 @@ export function RecordView<T extends { id: RowId }>({
1300
1312
  </TableHead>
1301
1313
  {visibleFields.map((f) => {
1302
1314
  const HeadIcon = f.icon ?? DEFAULT_FIELD_ICON;
1315
+ const sortable = canSort(f);
1316
+ const headInner = (
1317
+ <>
1318
+ <HeadIcon className="size-3.5 shrink-0 text-foreground" />
1319
+ <span className="flex items-center gap-1 whitespace-nowrap">
1320
+ {f.label}
1321
+ {f.required && <RequiredMark />}
1322
+ </span>
1323
+ {sort?.key === f.key &&
1324
+ (sort.dir === "asc" ? (
1325
+ <ArrowUp className="size-3 shrink-0" />
1326
+ ) : (
1327
+ <ArrowDown className="size-3 shrink-0" />
1328
+ ))}
1329
+ </>
1330
+ );
1331
+ const headClass = cn(
1332
+ "flex h-8 w-full items-center gap-1.5 whitespace-nowrap",
1333
+ ALIGN_BOX[alignOf(f.key)],
1334
+ );
1303
1335
  return (
1304
1336
  <TableHead
1305
1337
  key={f.key}
1306
1338
  className="relative w-max"
1307
1339
  style={{ width: colWidths[f.key] }}
1308
1340
  >
1309
- <button
1310
- type="button"
1311
- onClick={() => toggleSort(f.key)}
1312
- className={cn(
1313
- "flex h-8 w-full items-center gap-1.5 whitespace-nowrap hover:text-foreground",
1314
- ALIGN_BOX[alignOf(f.key)],
1315
- )}
1316
- >
1317
- <HeadIcon className="size-3.5 shrink-0 text-foreground" />
1318
- <span className="flex items-center gap-1 whitespace-nowrap">
1319
- {f.label}
1320
- {f.required && <RequiredMark />}
1321
- </span>
1322
- {sort?.key === f.key &&
1323
- (sort.dir === "asc" ? (
1324
- <ArrowUp className="size-3 shrink-0" />
1325
- ) : (
1326
- <ArrowDown className="size-3 shrink-0" />
1327
- ))}
1328
- </button>
1341
+ {sortable ? (
1342
+ <button
1343
+ type="button"
1344
+ onClick={() => toggleSort(f.key)}
1345
+ className={cn(headClass, "hover:text-foreground")}
1346
+ >
1347
+ {headInner}
1348
+ </button>
1349
+ ) : (
1350
+ // Not sortable: a static label, no toggle / hover affordance.
1351
+ <span className={headClass}>{headInner}</span>
1352
+ )}
1329
1353
  {resizeHandle(f.key, f.label)}
1330
1354
  </TableHead>
1331
1355
  );
@@ -21,6 +21,9 @@ const fields: RecordField<Region>[] = [
21
21
  required: true,
22
22
  group: "General",
23
23
  hideInTable: true,
24
+ // No Name column (shown in the identity cell via getPrimary), but still
25
+ // sortable from the Sort dropdown — `sortable` decouples it from visibility.
26
+ sortable: true,
24
27
  filterable: true,
25
28
  },
26
29
  {