diff --git a/resources/js/components/transactions/transaction-list.test.tsx b/resources/js/components/transactions/transaction-list.test.tsx
new file mode 100644
index 00000000..2bde144b
--- /dev/null
+++ b/resources/js/components/transactions/transaction-list.test.tsx
@@ -0,0 +1,23 @@
+import { render } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { TransactionListSkeleton } from './transaction-list';
+
+describe('TransactionListSkeleton', () => {
+ it('renders the table-shaped skeleton shared by the deferred fallback and the internal loading state', () => {
+ const { container } = render();
+
+ // The table-shaped skeleton is wrapped in a bordered container, which is
+ // what distinguishes it from the old plain-bars fallback. Using the same
+ // component in both loading phases is what removes the double-skeleton jump.
+ expect(
+ container.querySelector('.overflow-hidden.rounded-md.border'),
+ ).not.toBeNull();
+
+ // Far more placeholders than the old 8-bar fallback: header row, section
+ // header, and six body rows, each mimicking the real table layout.
+ expect(
+ container.querySelectorAll('[data-slot="skeleton"]').length,
+ ).toBeGreaterThan(8);
+ });
+});
diff --git a/resources/js/components/transactions/transaction-list.tsx b/resources/js/components/transactions/transaction-list.tsx
index a1a5d088..d8685fcb 100644
--- a/resources/js/components/transactions/transaction-list.tsx
+++ b/resources/js/components/transactions/transaction-list.tsx
@@ -81,6 +81,41 @@ import { UUID } from '@/types/uuid';
const COLUMN_VISIBILITY_KEY = 'transactions-column-visibility';
+export function TransactionListSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
+
interface TransactionRowProps {
row: Row;
virtualRow: VirtualItem;
@@ -278,7 +313,9 @@ export function TransactionList({
const [transactions, setTransactions] = useState(
[],
);
- const [isLoading, setIsLoading] = useState(true);
+ const [isLoading, setIsLoading] = useState(
+ providedTransactions === undefined,
+ );
const [sorting, setSorting] = useState([
{ id: 'transaction_date', desc: true },
]);
@@ -1005,39 +1042,7 @@ export function TransactionList({
/>
{isLoading || isSearching ? (
-
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 6 }).map((_, index) => (
-
- ))}
-
-
-
-
-
-
-
+
) : (
<>
({
vi.mock('@/components/transactions/transaction-list', () => ({
TransactionList: () => null,
+ TransactionListSkeleton: () => null,
}));
vi.mock('@/components/bank-logo', () => ({
diff --git a/resources/js/pages/Accounts/Show.tsx b/resources/js/pages/Accounts/Show.tsx
index c8ded843..d534ce39 100644
--- a/resources/js/pages/Accounts/Show.tsx
+++ b/resources/js/pages/Accounts/Show.tsx
@@ -17,7 +17,10 @@ import HeadingSmall from '@/components/heading-small';
import InputError from '@/components/input-error';
import { MobileBackButton } from '@/components/mobile-back-button';
import { EditTransactionDialog } from '@/components/transactions/edit-transaction-dialog';
-import { TransactionList } from '@/components/transactions/transaction-list';
+import {
+ TransactionList,
+ TransactionListSkeleton,
+} from '@/components/transactions/transaction-list';
import { AmountDisplay } from '@/components/ui/amount-display';
import { AmountInput } from '@/components/ui/amount-input';
import { Button } from '@/components/ui/button';
@@ -39,7 +42,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
-import { Skeleton } from '@/components/ui/skeleton';
import { Textarea } from '@/components/ui/textarea';
import { useChartColors } from '@/hooks/use-chart-color-scheme';
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
@@ -385,13 +387,7 @@ export default function AccountShow({
{isTransactionalAccount(account) && (
- {Array.from({ length: 8 }).map((_, i) => (
-
- ))}
-
- }
+ fallback={}
>