@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 +2 -0
- package/CHANGELOG.md +23 -0
- package/package.json +1 -1
- package/src/record-view.tsx +47 -11
- 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,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.
|
|
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
|
@@ -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
|
-
|
|
14
|
-
CaretSortIcon as
|
|
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={<
|
|
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
|
-
<
|
|
1291
|
+
<CaretUp className="size-3.5" />
|
|
1287
1292
|
) : (
|
|
1288
|
-
<
|
|
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
|
-
{
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
-
<
|
|
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
|
-
{
|
|
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={
|
|
74
|
+
initialData={data}
|
|
75
|
+
data={data}
|
|
76
|
+
onDataChange={setData}
|
|
61
77
|
getPrimary={(row) => ({
|
|
62
78
|
title: row.name,
|
|
63
79
|
subtitle: row.organization,
|