@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 +2 -0
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/record-view.tsx +30 -1
- package/template/app/(app)/markets/markets-table.tsx +17 -1
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.
|
|
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",
|
package/src/record-view.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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={
|
|
74
|
+
initialData={data}
|
|
75
|
+
data={data}
|
|
76
|
+
onDataChange={setData}
|
|
61
77
|
getPrimary={(row) => ({
|
|
62
78
|
title: row.name,
|
|
63
79
|
subtitle: row.organization,
|