- {header}
- {/* Skeleton — shape-aware: matches a real TxRow (icon · 2-line
- description · amount). Stagger fades down so the list feels
- like it's "loading from the top" instead of pulsing as a slab. */}
- {loading && activeTx.length === 0 && (
-
- {Array(10)
- .fill(0)
- .map((_, i) => (
-
- ))}
-
- )}
+ // DataState contract:
+ // - `data === undefined` triggers the skeleton slot. We mark the
+ // query as still-loading only on the very first paint, when the
+ // parent month list is empty (`activeTx.length === 0`); subsequent
+ // background refetches keep `data` defined so the existing list
+ // stays visible while a stale-revalidate happens.
+ // - `isEmpty` reads the post-filter list (`filtered`) so the empty
+ // slot shows when filters/exclusions hide every row, not when the
+ // month payload itself is empty (which the skeleton already covers
+ // during the first paint).
+ const txQuery: DataStateQueryLike
= {
+ data: loading && activeTx.length === 0 ? undefined : filtered,
+ isLoading: loading,
+ };
- {/* Empty */}
- {filtered.length === 0 && !loading && (
-
-
}
- title="Немає транзакцій"
- description="Зміни місяць, фільтр або переключи «приховані», якщо вони є."
+ const skeleton = (
+ // Skeleton — shape-aware: matches a real TxRow (icon · 2-line
+ // description · amount). Stagger fades down so the list feels
+ // like it's "loading from the top" instead of pulsing as a slab.
+
+ {Array(10)
+ .fill(0)
+ .map((_, i) => (
+
-
- )}
+ ))}
+
+ );
- {/* Virtualized list */}
- {filtered.length > 0 && (
-
- {
- const group = groupedByDate[groupIndex];
- if (!group) return null;
- const key = group.key;
- const collapsed = collapsedKeys.has(key);
- const summary = daySummaries[key] ?? {
- total: 0,
- count: 0,
- statCount: 0,
- };
- // Коли у день є тільки «не в статистиці» транзакції, сховати
- // суму — інакше побачимо «0,00₴» або (як раніше) злиплі
- // перекази у вигляді доходу.
- const showTotal = showBalance && summary.statCount > 0;
- return (
-
- );
- }}
- itemContent={(index) => {
- const t = flatItems[index];
- if (!t) return null;
- const rowTx = t as TxRowTx;
- return (
- onSwipeDeleteManual(t)}
- onEditManual={onEditManual}
- onHideTx={onHideTx}
- onCatChange={onCatChange}
- onSplitChange={(id, splits) =>
- onSplitChange(id, (splits ?? []) as TxSplit[])
- }
- />
- );
- }}
- />
-
- )}
+ const emptyFallback = (
+
+ }
+ title="Немає транзакцій"
+ description="Зміни місяць, фільтр або переключи «приховані», якщо вони є."
+ />
+
+ );
+
+ const content = (
+
+ {header}
+
data.length === 0}
+ >
+ {() => (
+
+ {
+ const group = groupedByDate[groupIndex];
+ if (!group) return null;
+ const key = group.key;
+ const collapsed = collapsedKeys.has(key);
+ const summary = daySummaries[key] ?? {
+ total: 0,
+ count: 0,
+ statCount: 0,
+ };
+ // Коли у день є тільки «не в статистиці» транзакції, сховати
+ // суму — інакше побачимо «0,00₴» або (як раніше) злиплі
+ // перекази у вигляді доходу.
+ const showTotal = showBalance && summary.statCount > 0;
+ return (
+
+ );
+ }}
+ itemContent={(index) => {
+ const t = flatItems[index];
+ if (!t) return null;
+ const rowTx = t as TxRowTx;
+ return (
+ onSwipeDeleteManual(t)}
+ onEditManual={onEditManual}
+ onHideTx={onHideTx}
+ onCatChange={onCatChange}
+ onSplitChange={(id, splits) =>
+ onSplitChange(id, (splits ?? []) as TxSplit[])
+ }
+ />
+ );
+ }}
+ />
+
+ )}
+
{trailing}