@viliha/vui-ui 1.11.1 → 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,19 @@ 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
+
10
23
  ## 1.11.1 — 2026-07-26
11
24
 
12
25
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viliha/vui-ui",
3
- "version": "1.11.1",
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",
@@ -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.
@@ -1458,7 +1463,31 @@ export function RecordView<T extends { id: RowId }>({
1458
1463
  </TableRow>
1459
1464
  </TableHeader>
1460
1465
  <TableBody>
1461
- {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 ? (
1462
1491
  paged.map((row) => {
1463
1492
  const primary = getPrimary(row);
1464
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,