@viliha/vui-ui 1.11.0 → 1.12.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
@@ -338,6 +338,8 @@ Sorting follows column visibility by default; use `sortable` to decouple them
338
338
 
339
339
  Form controls come from the field: `options` → `Select`, `input: "combobox"` → a searchable `Combobox` (`@viliha/vui-ui/combobox`, use for long lists), `input: "number" | "date"` → native inputs. For anything the built-ins don't cover (checkbox, radio group, slider, custom widget), set `renderInput({ value, onChange, field, invalid })` on the field — it drops any component into the Add/Edit form while the field keeps its label, required mark, and Save validation. Don't hand-build a `<form>`.
340
340
 
341
+ Loading from a server: pass `loading` to `RecordView` while the fetch is in flight — it shows animated skeleton rows (matched to the columns) instead of an empty-state flash; clear it when the data arrives. Don't build your own spinner overlay.
342
+
341
343
  Dependent/cascading options: make `options` (form) or `filterable.options` (filter) a function — `(draft) => …` in the form, `(values) => …` in the filter — and one field's choices depend on another; RecordView clears the child when the parent change invalidates it. Keep `options` a static array for the bulk "Set {label}" action. If a field has both a function `options` and `renderInput`, guard with `Array.isArray(field.options)` before mapping (renderInput doesn't get the draft to resolve it).
342
344
 
343
345
  ## Filtering
package/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ 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.12.0 — 2026-07-26
11
+
12
+ ### Added
13
+
14
+ - **`RecordView` `loading` prop** — while `true`, the table body renders animated
15
+ skeleton rows (matched to the columns) instead of an empty-state flash, for
16
+ slow server loads (initial fetch or refetch). The toolbar stays usable; clear
17
+ `loading` when the data arrives.
18
+
19
+ **For agents:** wrap your fetch with `loading` (`setLoading(true)` → fetch →
20
+ `finally(() => setLoading(false))`); don't hand-roll a spinner overlay. Demo:
21
+ `markets` simulates a load on first visit. Docs: `/docs/data-table`.
22
+
23
+ ## 1.11.1 — 2026-07-26
24
+
25
+ ### Changed
26
+
27
+ - **Sort indicators are now carets, and every sortable column shows one.** A
28
+ sortable column header shows a muted up/down caret (`CaretSort`) by default —
29
+ so sortability is discoverable — and a solid caret for the active direction:
30
+ `CaretUp` = ascending, `CaretDown` = descending (all Radix icons). The Sort
31
+ dropdown matches. Non-sortable columns (`sortable: false`) show no indicator.
32
+
10
33
  ## 1.11.0 — 2026-07-26
11
34
 
12
35
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viliha/vui-ui",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",
@@ -8,10 +8,10 @@ import {
8
8
  ReaderIcon as Reader,
9
9
  TableIcon as SheetIcon,
10
10
  UploadIcon as Upload,
11
- ArrowDownIcon as ArrowDown,
12
11
  ArrowTopRightIcon as ArrowUpRight,
13
- ArrowUpIcon as ArrowUp,
14
- CaretSortIcon as ArrowUpDown,
12
+ CaretDownIcon as CaretDown,
13
+ CaretSortIcon as CaretSort,
14
+ CaretUpIcon as CaretUp,
15
15
  CheckIcon as Check,
16
16
  ChevronLeftIcon as ChevronLeft,
17
17
  ChevronRightIcon as ChevronRight,
@@ -380,6 +380,10 @@ interface RecordViewProps<T extends { id: RowId }> {
380
380
  * or client-side filtering here. In per-field mode the panel does not match
381
381
  * rows itself, so the behavior is entirely yours. */
382
382
  onFilter?: (values: FilterValues<T>) => void;
383
+ /** Show animated skeleton rows instead of the table body while data loads
384
+ * from the server (an initial fetch or a filter/refetch). Set it around your
385
+ * async load; the toolbar stays usable. */
386
+ loading?: boolean;
383
387
  }
384
388
 
385
389
  export function RecordView<T extends { id: RowId }>({
@@ -402,6 +406,7 @@ export function RecordView<T extends { id: RowId }>({
402
406
  persistKey,
403
407
  resizableColumns = false,
404
408
  onFilter,
409
+ loading = false,
405
410
  }: RecordViewProps<T>) {
406
411
  const { titleLeading } = React.useContext(PageChromeContext);
407
412
  // Surface the page title/icon in the app's global top bar.
@@ -1274,7 +1279,7 @@ export function RecordView<T extends { id: RowId }>({
1274
1279
  )}
1275
1280
  </Dropdown>
1276
1281
 
1277
- <Dropdown label="Sort" icon={<ArrowUpDown className="size-3.5 text-blue-500" />}>
1282
+ <Dropdown label="Sort" icon={<CaretSort className="size-3.5 text-blue-500" />}>
1278
1283
  <DropdownLabel>Sort by</DropdownLabel>
1279
1284
  {sortFields.map((f) => (
1280
1285
  <DropdownItem
@@ -1283,9 +1288,9 @@ export function RecordView<T extends { id: RowId }>({
1283
1288
  icon={
1284
1289
  sort?.key === f.key ? (
1285
1290
  sort.dir === "asc" ? (
1286
- <ArrowUp className="size-3.5" />
1291
+ <CaretUp className="size-3.5" />
1287
1292
  ) : (
1288
- <ArrowDown className="size-3.5" />
1293
+ <CaretDown className="size-3.5" />
1289
1294
  )
1290
1295
  ) : undefined
1291
1296
  }
@@ -1402,11 +1407,18 @@ export function RecordView<T extends { id: RowId }>({
1402
1407
  {f.label}
1403
1408
  {f.required && <RequiredMark />}
1404
1409
  </span>
1405
- {sort?.key === f.key &&
1406
- (sort.dir === "asc" ? (
1407
- <ArrowUp className="size-3 shrink-0" />
1410
+ {/* Sortable columns always show an indicator: a muted
1411
+ up/down caret by default, a solid caret for the active
1412
+ direction (up = ascending, down = descending). */}
1413
+ {sortable &&
1414
+ (sort?.key === f.key ? (
1415
+ sort.dir === "asc" ? (
1416
+ <CaretUp className="size-3.5 shrink-0" />
1417
+ ) : (
1418
+ <CaretDown className="size-3.5 shrink-0" />
1419
+ )
1408
1420
  ) : (
1409
- <ArrowDown className="size-3 shrink-0" />
1421
+ <CaretSort className="size-3.5 shrink-0 text-muted-foreground/50" />
1410
1422
  ))}
1411
1423
  </>
1412
1424
  );
@@ -1451,7 +1463,31 @@ export function RecordView<T extends { id: RowId }>({
1451
1463
  </TableRow>
1452
1464
  </TableHeader>
1453
1465
  <TableBody>
1454
- {processed.length ? (
1466
+ {loading ? (
1467
+ // Animated skeleton rows while data loads from the server.
1468
+ Array.from({ length: Math.min(pageSize, 8) }).map((_, i) => (
1469
+ <TableRow key={`skeleton-${i}`} className="hover:bg-transparent">
1470
+ <TableCell style={{ width: CHECKBOX_W }}>
1471
+ <div className="mx-2 size-4 animate-pulse rounded bg-muted" />
1472
+ </TableCell>
1473
+ <TableCell style={{ width: colWidths[NAME_COL] }}>
1474
+ <div className="flex items-center gap-2">
1475
+ <div className="size-7 shrink-0 animate-pulse rounded-full bg-muted" />
1476
+ <div className="h-3.5 w-32 animate-pulse rounded bg-muted" />
1477
+ </div>
1478
+ </TableCell>
1479
+ {visibleFields.map((f) => (
1480
+ <TableCell key={f.key} style={{ width: colWidths[f.key] }}>
1481
+ <div className="h-3.5 w-20 animate-pulse rounded bg-muted" />
1482
+ </TableCell>
1483
+ ))}
1484
+ <TableCell aria-hidden="true" className="border-r-0" />
1485
+ <TableCell style={{ width: ACTIONS_W }}>
1486
+ <div className="mx-auto h-4 w-8 animate-pulse rounded bg-muted" />
1487
+ </TableCell>
1488
+ </TableRow>
1489
+ ))
1490
+ ) : processed.length ? (
1455
1491
  paged.map((row) => {
1456
1492
  const primary = getPrimary(row);
1457
1493
  return (
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useEffect, useState } from "react";
3
4
  import {
4
5
  CubeIcon as Building,
5
6
  GlobeIcon as Compass,
@@ -51,13 +52,28 @@ const fields: RecordField<Market>[] = [
51
52
  ];
52
53
 
53
54
  export function MarketsTable() {
55
+ // Demo: simulate loading records from a server so the skeleton state shows on
56
+ // first visit. In a real app, set `loading` around your fetch/refetch.
57
+ const [data, setData] = useState<Market[]>([]);
58
+ const [loading, setLoading] = useState(true);
59
+ useEffect(() => {
60
+ const t = setTimeout(() => {
61
+ setData(markets);
62
+ setLoading(false);
63
+ }, 900);
64
+ return () => clearTimeout(t);
65
+ }, []);
66
+
54
67
  return (
55
68
  <RecordView
56
69
  title="Markets"
57
70
  singular="Market"
58
71
  icon={MapPin}
72
+ loading={loading}
59
73
  fields={fields}
60
- initialData={markets}
74
+ initialData={data}
75
+ data={data}
76
+ onDataChange={setData}
61
77
  getPrimary={(row) => ({
62
78
  title: row.name,
63
79
  subtitle: row.organization,