@viliha/vui-ui 1.14.0 → 1.14.2
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/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/record-view.tsx +36 -11
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ 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.14.2 — 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Consistent loading feedback in `fetcher` mode.** A cache hit is served from
|
|
15
|
+
the in-memory cache (no server round-trip), but the loading shimmer now stays
|
|
16
|
+
up for a short minimum (300ms) so a cached load looks the same as a real
|
|
17
|
+
fetch — no confusing "loading but blank" flash on tab switch. Real fetches
|
|
18
|
+
longer than the minimum are unaffected; background (post-mutation) refetches
|
|
19
|
+
stay silent.
|
|
20
|
+
|
|
21
|
+
Note: the `fetcher` cache (added in 1.14.0) stores each page's data in a
|
|
22
|
+
module-scoped `Map` in JS memory, so switching tabs serves from memory and does
|
|
23
|
+
**not** re-hit the server. If you still see a refetch, you're on a build older
|
|
24
|
+
than 1.14.0 (the demo before that had no cache).
|
|
25
|
+
|
|
26
|
+
## 1.14.1 — 2026-07-26
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Per-field Filter panel layout.** The header ("Filter") and the footer
|
|
31
|
+
(Clear / Search) are now static — only the fields scroll — so the actions stay
|
|
32
|
+
in view on a long filter form. The footer's top border spans the full panel
|
|
33
|
+
width, and the action buttons are compact (`size="sm"`).
|
|
34
|
+
|
|
10
35
|
## 1.14.0 — 2026-07-26
|
|
11
36
|
|
|
12
37
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viliha/vui-ui",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
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
|
@@ -359,6 +359,11 @@ function resolveOptions<T>(
|
|
|
359
359
|
type RvCacheEntry = { rows: unknown[]; total: number; at: number };
|
|
360
360
|
const RV_CACHE = new Map<string, Map<string, RvCacheEntry>>();
|
|
361
361
|
|
|
362
|
+
// Minimum time the loading shimmer stays up per `fetcher` load, so a cache hit
|
|
363
|
+
// (instant, from memory) shows the same animation as a real fetch — consistent
|
|
364
|
+
// feedback instead of a blank flash. Real fetches longer than this are unaffected.
|
|
365
|
+
const RV_MIN_LOADING_MS = 300;
|
|
366
|
+
|
|
362
367
|
function rvQueryKey<T>(q: ServerQuery<T>): string {
|
|
363
368
|
return JSON.stringify([q.page, q.pageSize, q.sort, q.search, q.filters]);
|
|
364
369
|
}
|
|
@@ -528,17 +533,32 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
528
533
|
const runFetch = React.useCallback(
|
|
529
534
|
(q: ServerQuery<T>, opts?: { background?: boolean }) => {
|
|
530
535
|
if (!fetcher) return;
|
|
531
|
-
|
|
536
|
+
const id = ++reqIdRef.current;
|
|
537
|
+
const started = Date.now();
|
|
538
|
+
// Reveal the data, but hold the shimmer for a consistent minimum so a
|
|
539
|
+
// cache hit (served from memory, no server call) looks the same as a real
|
|
540
|
+
// fetch — same animation every time, never a confusing blank flash.
|
|
541
|
+
const commit = (rows: T[], total: number) => {
|
|
542
|
+
const apply = () => {
|
|
543
|
+
if (id !== reqIdRef.current) return; // superseded
|
|
544
|
+
setFetchedData(rows);
|
|
545
|
+
setFetchedTotal(total);
|
|
546
|
+
setFetchedLoading(false);
|
|
547
|
+
};
|
|
548
|
+
const wait = RV_MIN_LOADING_MS - (Date.now() - started);
|
|
549
|
+
if (opts?.background || wait <= 0) apply();
|
|
550
|
+
else window.setTimeout(apply, wait);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// Foreground cache hit: no server round-trip — the data is in memory.
|
|
532
554
|
if (!opts?.background && cacheKey) {
|
|
533
555
|
const hit = rvCacheGet(cacheKey, rvQueryKey(q), ttlMs);
|
|
534
556
|
if (hit) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
setFetchedLoading(false);
|
|
557
|
+
setFetchedLoading(true);
|
|
558
|
+
commit(hit.rows as T[], hit.total);
|
|
538
559
|
return;
|
|
539
560
|
}
|
|
540
561
|
}
|
|
541
|
-
const id = ++reqIdRef.current;
|
|
542
562
|
abortRef.current?.abort();
|
|
543
563
|
const controller = new AbortController();
|
|
544
564
|
abortRef.current = controller;
|
|
@@ -553,9 +573,7 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
553
573
|
{ rows: res.rows, total: res.total, at: Date.now() },
|
|
554
574
|
cacheMax,
|
|
555
575
|
);
|
|
556
|
-
|
|
557
|
-
setFetchedTotal(res.total);
|
|
558
|
-
setFetchedLoading(false);
|
|
576
|
+
commit(res.rows, res.total);
|
|
559
577
|
})
|
|
560
578
|
.catch((err) => {
|
|
561
579
|
if (controller.signal.aborted || id !== reqIdRef.current) return;
|
|
@@ -1363,8 +1381,11 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
1363
1381
|
// Search / Clear. The panel only gathers values — matching is the
|
|
1364
1382
|
// consumer's job via `onFilter` (see the field's `filterable`).
|
|
1365
1383
|
<>
|
|
1366
|
-
<
|
|
1367
|
-
|
|
1384
|
+
<div className="flex max-h-[min(28rem,70vh)] w-72 flex-col">
|
|
1385
|
+
{/* Header: static, full-width separator (from DropdownLabel). */}
|
|
1386
|
+
<DropdownLabel>Filter</DropdownLabel>
|
|
1387
|
+
{/* Content: the only scrolling region. */}
|
|
1388
|
+
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3">
|
|
1368
1389
|
{filterFields.map((f) => {
|
|
1369
1390
|
const cfg: FieldFilter<T> =
|
|
1370
1391
|
typeof f.filterable === "object" ? f.filterable : {};
|
|
@@ -1453,8 +1474,11 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
1453
1474
|
</div>
|
|
1454
1475
|
);
|
|
1455
1476
|
})}
|
|
1456
|
-
|
|
1477
|
+
</div>
|
|
1478
|
+
{/* Footer: static, full-width top border, compact buttons. */}
|
|
1479
|
+
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-border p-3">
|
|
1457
1480
|
<Button
|
|
1481
|
+
size="sm"
|
|
1458
1482
|
onClick={() => {
|
|
1459
1483
|
setFilterValues({});
|
|
1460
1484
|
onFilter?.({});
|
|
@@ -1473,6 +1497,7 @@ export function RecordView<T extends { id: RowId }>({
|
|
|
1473
1497
|
Clear
|
|
1474
1498
|
</Button>
|
|
1475
1499
|
<Button
|
|
1500
|
+
size="sm"
|
|
1476
1501
|
variant="primary"
|
|
1477
1502
|
onClick={() => {
|
|
1478
1503
|
onFilter?.(filterValues);
|