From 134b6986bcbc9558b1820cf73b23b0f865a39642 Mon Sep 17 00:00:00 2001 From: Yusef Habib Fernandez Date: Tue, 25 Aug 2026 07:55:33 +0200 Subject: [PATCH] ux(loading): make every loading state match the content it replaces The app had six ways to show a load. Three of them showed generic grey bars that did not match the page behind them. Replace them with one set of content shaped shells, move the anti flicker delay into CSS so every skeleton gets it, and route the raw spinner icons through the shared `Spinner`. --- .../src/app/styles/main.css | 59 +++++++++++++ .../common/components/MultipleSkeletons.tsx | 15 ---- .../src/common/components/MutationDialog.tsx | 18 +--- .../common/components/ProcessingSpinner.tsx | 20 +++++ .../src/common/components/QueryStates.tsx | 11 ++- .../src/common/components/Skeleton.tsx | 14 ++- .../components/skeletons/AccountsSkeleton.tsx | 49 +++++++++++ .../skeletons/DashboardSkeleton.tsx | 86 +++++++++++++++++++ .../components/skeletons/NeuronsSkeleton.tsx | 57 ++++++++++++ .../skeletons/ProposalDetailSkeleton.tsx | 51 +++++++++++ .../skeletons/ProposalListSkeleton.tsx | 44 ++++++++++ .../components/skeletons/SettingsSkeleton.tsx | 34 ++++++++ .../skeletons/SkeletonAccountCard.tsx | 26 ++++++ .../components/skeletons/SkeletonListRows.tsx | 55 ++++++++++++ .../skeletons/SkeletonPageHeader.tsx | 18 ++++ .../components/skeletons/SkeletonScreen.tsx | 19 ++++ .../components/skeletons/SkeletonStatCard.tsx | 20 +++++ .../components/skeletons/SkeletonText.tsx | 35 ++++++++ .../skeletons/SkeletonTransactionList.tsx | 49 +++++++++++ .../components/skeletons/VotingSkeleton.tsx | 33 +++++++ .../src/common/constants/extra.ts | 4 - .../src/common/hooks/useDelayedFlag.ts | 34 -------- .../account/components/AccountCard.tsx | 2 +- .../accounts/components/AccountsCard.tsx | 8 +- .../accounts/components/AccountsList.tsx | 11 +-- .../accounts/components/AccountsListItem.tsx | 2 +- .../accounts/components/AccountsTotalCard.tsx | 6 +- .../components/RecentTransactions.tsx | 13 +-- .../components/AddressBookModal.tsx | 13 +-- .../dashboard/components/DailyRewardsCard.tsx | 24 +++--- .../dashboard/components/IcpPriceCard.tsx | 29 ++++--- .../dashboard/components/TotalStakedCard.tsx | 43 +++++----- .../components/ProposalDetailsVoting.tsx | 7 +- .../proposals/components/ProposalListItem.tsx | 6 +- .../components/FollowingStatusAlert.tsx | 5 +- .../features/stakes/components/StakedCard.tsx | 4 +- .../neuronDetail/NeuronDetailDissolveView.tsx | 5 +- .../NeuronDetailIncreaseDelayView.tsx | 5 +- .../NeuronDetailIncreaseStakeView.tsx | 5 +- .../NeuronDetailMaturityModeView.tsx | 5 +- .../StakingWizardStepConfirmation.tsx | 18 +--- .../components/summaryCards/CapitalCard.tsx | 29 +++---- .../components/summaryCards/EarningsCard.tsx | 2 +- .../summaryCards/EngagementCard.tsx | 4 +- .../components/TransactionListDialog.tsx | 8 +- .../components/AdvancedFollowingModal.tsx | 23 ++--- .../voting/components/FollowedNeuronCard.tsx | 2 +- .../voting/components/FolloweePicker.tsx | 11 +-- .../voting/components/KnownNeuronCard.tsx | 4 +- .../components/SimpleFollowingModal.tsx | 11 +-- .../components/VotingOverviewAdvanced.tsx | 21 ++--- .../components/VotingOverviewSimple.tsx | 20 +++-- .../src/routes/_auth/accounts/index.tsx | 2 + .../src/routes/_auth/dashboard/index.tsx | 2 + .../src/routes/_auth/neurons/index.tsx | 3 + .../src/routes/_auth/settings/index.tsx | 2 + .../src/routes/_auth/voting/index.tsx | 23 ++--- .../_auth/voting/proposals/$id/index.tsx | 8 +- 58 files changed, 851 insertions(+), 286 deletions(-) delete mode 100644 src/governance-app-frontend/src/common/components/MultipleSkeletons.tsx create mode 100644 src/governance-app-frontend/src/common/components/ProcessingSpinner.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/AccountsSkeleton.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/DashboardSkeleton.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/NeuronsSkeleton.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/ProposalDetailSkeleton.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/ProposalListSkeleton.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SettingsSkeleton.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonAccountCard.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonListRows.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonPageHeader.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonScreen.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonStatCard.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonText.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/SkeletonTransactionList.tsx create mode 100644 src/governance-app-frontend/src/common/components/skeletons/VotingSkeleton.tsx delete mode 100644 src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts diff --git a/src/governance-app-frontend/src/app/styles/main.css b/src/governance-app-frontend/src/app/styles/main.css index 292779331..88f64a89e 100644 --- a/src/governance-app-frontend/src/app/styles/main.css +++ b/src/governance-app-frontend/src/app/styles/main.css @@ -61,6 +61,11 @@ --sidebar-ring: var(--icp-accent-strong); --main-content: var(--icp-bg); --staking-ratio: var(--icp-section-teal); + /* Skeleton bars pulse between these two. The pulse animates a color, which + leaves `opacity` free for the delayed reveal. An ink wash on the parchment, + not a tint of the rust accent: a placeholder must not read as content. */ + --skeleton-base: rgba(26, 26, 26, 0.07); + --skeleton-peak: rgba(26, 26, 26, 0.035); } .dark { @@ -111,6 +116,8 @@ --sidebar-ring: var(--icp-accent-strong); --main-content: var(--icp-bg); --staking-ratio: #3fafaa; + --skeleton-base: rgba(162, 154, 141, 0.2); + --skeleton-peak: rgba(162, 154, 141, 0.1); } @theme inline { @@ -204,6 +211,58 @@ } @layer utilities { + /* + * Skeleton bars. + * + * A bar claims its space on the first frame, so the content lands without a + * jump. It stays invisible for `--skeleton-delay` first, so a fast query + * resolves into content with no grey flash on the way. + * + * The reveal animates `opacity` and the pulse animates `background-color`. + * Two animations on one property fight, and the pulse is declared last, so + * it would win and cancel the delay. + */ + @keyframes skeleton-reveal { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes skeleton-pulse { + 0%, + 100% { + background-color: var(--skeleton-base); + } + 50% { + background-color: var(--skeleton-peak); + } + } + + .skeleton { + --skeleton-delay: 120ms; + background-color: var(--skeleton-base); + animation: + skeleton-reveal 150ms ease-out var(--skeleton-delay) both, + skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) var(--skeleton-delay) infinite; + } + + /* + * Keep the delayed reveal, drop the pulse. Spinners keep turning: they are + * the only sign that a mutation is still running. + */ + @media (prefers-reduced-motion: reduce) { + .skeleton { + animation: skeleton-reveal 0s linear var(--skeleton-delay) both; + } + + .animate-pulse { + animation: none; + } + } + .text-hero-responsive { font-size: clamp(2rem, 5vh, 4rem); line-height: 1.1; diff --git a/src/governance-app-frontend/src/common/components/MultipleSkeletons.tsx b/src/governance-app-frontend/src/common/components/MultipleSkeletons.tsx deleted file mode 100644 index 0ec2480da..000000000 --- a/src/governance-app-frontend/src/common/components/MultipleSkeletons.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Skeleton } from '@components/Skeleton'; - -type Props = { - count: number; -}; - -export const MultipleSkeletons = ({ count }: Props) => { - return ( -
- {Array.from({ length: count }).map((_, index) => ( - - ))} -
- ); -}; diff --git a/src/governance-app-frontend/src/common/components/MutationDialog.tsx b/src/governance-app-frontend/src/common/components/MutationDialog.tsx index 726d62034..b022938c3 100644 --- a/src/governance-app-frontend/src/common/components/MutationDialog.tsx +++ b/src/governance-app-frontend/src/common/components/MutationDialog.tsx @@ -1,10 +1,11 @@ -import { AlertTriangle, Loader } from 'lucide-react'; +import { AlertTriangle } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@components/button'; import { NavigationBlockerDialog } from '@components/NavigationBlockerDialog'; +import { ProcessingSpinner } from '@components/ProcessingSpinner'; import { ResponsiveDialog, ResponsiveDialogContent, @@ -131,7 +132,7 @@ export function MutationDialog({ {processingMessage} - + {processingMessage} )} @@ -232,19 +233,6 @@ function PhaseContainer({ ); } -function AnimatedSpinner() { - return ( - - - - ); -} - function AnimatedSuccessIcon() { return ( ( + + + +); diff --git a/src/governance-app-frontend/src/common/components/QueryStates.tsx b/src/governance-app-frontend/src/common/components/QueryStates.tsx index 48bcf259b..6b31306e1 100644 --- a/src/governance-app-frontend/src/common/components/QueryStates.tsx +++ b/src/governance-app-frontend/src/common/components/QueryStates.tsx @@ -1,10 +1,8 @@ import { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { useDelayedFlag } from '@hooks/useDelayedFlag'; - import { EmptyMessage } from './EmptyMessage'; -import { MultipleSkeletons } from './MultipleSkeletons'; +import { SkeletonText } from './skeletons/SkeletonText'; import { WarningMessage } from './WarningMessage'; type InfiniteQueryData = Partial>; @@ -42,11 +40,12 @@ export const QueryStates = ({ }: Props) => { const { t } = useTranslation(); const q = query || infiniteQuery; - const showLoading = useDelayedFlag(q.isLoading); if (q.isLoading) { - // Hold the frame empty until the delay elapses rather than flashing a skeleton. - return showLoading ? loadingComponent || : null; + // The skeleton holds the space at once and reveals itself after a short + // delay, so a fast query needs no hold-back here. See `.skeleton` in + // `main.css`. Pass a `loadingComponent` built from `Skeleton` to keep that. + return loadingComponent || ; } if (q.error) { diff --git a/src/governance-app-frontend/src/common/components/Skeleton.tsx b/src/governance-app-frontend/src/common/components/Skeleton.tsx index ed5c6a156..05f698126 100644 --- a/src/governance-app-frontend/src/common/components/Skeleton.tsx +++ b/src/governance-app-frontend/src/common/components/Skeleton.tsx @@ -1,10 +1,22 @@ import { cn } from '@common/utils/shadcn'; +/** + * A placeholder bar. + * + * The look and the timing live in the `.skeleton` class in `main.css`: the bar + * holds its space from the first frame and stays invisible for a short delay, + * so a fast query resolves into content without a grey flash and without a + * jump. Pass size classes only; the colour comes from the theme. + * + * The bar is decorative. Wrap a group of them in `SkeletonScreen` so screen + * readers hear one "loading" for the region instead of one per bar. + */ function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { return (
); diff --git a/src/governance-app-frontend/src/common/components/skeletons/AccountsSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/AccountsSkeleton.tsx new file mode 100644 index 000000000..13a0a5eba --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/AccountsSkeleton.tsx @@ -0,0 +1,49 @@ +import { Card, CardContent, CardHeader } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonAccountCard } from './SkeletonAccountCard'; +import { SkeletonPageHeader } from './SkeletonPageHeader'; +import { SkeletonTransactionRows } from './SkeletonTransactionList'; + +/** + * Mirrors the `/accounts` layout: total card, account list, recent activity. + * + * `SkeletonTransactionRows` inside announces the load, so this wrapper stays a + * plain `div`. Nested status regions read twice. + */ +export const AccountsSkeleton = () => ( +
+ + + + + + + + + + +
+ + +
+
+
+ +
+
+ + +
+ + + + + + + + + +
+
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/DashboardSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/DashboardSkeleton.tsx new file mode 100644 index 000000000..e07b4f6f4 --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/DashboardSkeleton.tsx @@ -0,0 +1,86 @@ +import { Card, CardContent, CardHeader } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonScreen } from './SkeletonScreen'; +import { SkeletonStatCard } from './SkeletonStatCard'; +import { SkeletonText } from './SkeletonText'; + +/** Mirrors the `/dashboard` layout, so the real page lands in the same frame. */ +export const DashboardSkeleton = () => ( + + {/* SmartTitle */} +
+ + +
+ +
+ {/* TotalAssetsCard, with its radial chart. */} + + + + + + + + + + + {/* AccountCard or AccountsCard. */} + + + + + + + + + + + + {/* StakedCard. */} + + +
+ + +
+ + +
+ +
+ + +
+
+ + +
+
+
+
+ +
+ +
+ + + + +
+ + {/* ExecutiveSummaryCard. */} + + + + + + + + + + +
+
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/NeuronsSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/NeuronsSkeleton.tsx new file mode 100644 index 000000000..c65ba06ca --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/NeuronsSkeleton.tsx @@ -0,0 +1,57 @@ +import { Card, CardContent, CardHeader } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonPageHeader } from './SkeletonPageHeader'; +import { SkeletonScreen } from './SkeletonScreen'; +import { SkeletonStatCard } from './SkeletonStatCard'; + +/** One card in the neurons grid. */ +const SkeletonNeuronCard = () => ( + + +
+ + +
+ + +
+ +
+ + +
+ +
+
+); + +/** + * The summary row and the neuron grid, without the page header. + * + * The page keeps its own header while the neurons query runs, so this stands in + * for the part below it. + */ +export const NeuronsContentSkeleton = () => ( + +
+ + + + +
+ +
+ + +
+
+); + +/** Mirrors the whole `/neurons` layout. */ +export const NeuronsSkeleton = () => ( +
+ + +
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/ProposalDetailSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/ProposalDetailSkeleton.tsx new file mode 100644 index 000000000..19d968a2c --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/ProposalDetailSkeleton.tsx @@ -0,0 +1,51 @@ +import { Card, CardContent, CardHeader } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonScreen } from './SkeletonScreen'; +import { SkeletonText } from './SkeletonText'; + +/** + * The summary card and the body card, without the back link. + * + * The page renders its own back link while the proposal query runs, so this + * stands in for the part below it. + */ +export const ProposalDetailContentSkeleton = () => ( + + + +
+ + +
+ +
+ + + +
+
+ + +
+ + +
+
+
+ + + + + + +
+); + +/** Mirrors a single proposal page: back link, summary card, body card. */ +export const ProposalDetailSkeleton = () => ( +
+ + +
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/ProposalListSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/ProposalListSkeleton.tsx new file mode 100644 index 000000000..b2d4e0a8d --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/ProposalListSkeleton.tsx @@ -0,0 +1,44 @@ +import { Card, CardHeader } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonScreen } from './SkeletonScreen'; + +type Props = { + count?: number; +}; + +/** One row of the proposals list, badges and vote bar included. */ +const SkeletonProposalCard = () => ( + + +
+ + +
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+); + +/** Stands in for the proposals list, in the page and in the infinite scroll. */ +export const ProposalListSkeleton = ({ count = 3 }: Props) => ( + + {Array.from({ length: count }).map((_, index) => ( + + ))} + +); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SettingsSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/SettingsSkeleton.tsx new file mode 100644 index 000000000..17970836a --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SettingsSkeleton.tsx @@ -0,0 +1,34 @@ +import { Card } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonPageHeader } from './SkeletonPageHeader'; +import { SkeletonScreen } from './SkeletonScreen'; + +const SkeletonSection = ({ rows }: { rows: number }) => ( +
+ + +
+ {Array.from({ length: rows }).map((_, index) => ( +
+
+ + +
+ +
+ ))} +
+
+
+); + +/** Mirrors the `/settings` layout: stacked sections of divided rows. */ +export const SettingsSkeleton = () => ( + + + + + + +); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonAccountCard.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonAccountCard.tsx new file mode 100644 index 000000000..9c5f3b3f0 --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonAccountCard.tsx @@ -0,0 +1,26 @@ +import { Card, CardContent, CardHeader } from '@components/Card'; +import { Separator } from '@components/Separator'; +import { Skeleton } from '@components/Skeleton'; + +/** Mirrors `AccountsListItem`: name, account id, balance, last transaction. */ +export const SkeletonAccountCard = () => ( + + +
+
+ + +
+ +
+
+ +
+ + +
+ + +
+
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonListRows.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonListRows.tsx new file mode 100644 index 000000000..86cf3fcbb --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonListRows.tsx @@ -0,0 +1,55 @@ +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonScreen } from './SkeletonScreen'; + +type Props = { + count?: number; +}; + +/** Mirrors `AddressBookEntry`: name, address, two icon buttons, in a box. */ +export const SkeletonAddressBookRows = ({ count = 3 }: Props) => ( + + {Array.from({ length: count }).map((_, index) => ( +
+
+ + +
+
+ + +
+
+ ))} +
+); + +/** Mirrors a picker row: leading control plus a label. */ +export const SkeletonPickerRows = ({ count = 3 }: Props) => ( + + {Array.from({ length: count }).map((_, index) => ( +
+ + +
+ ))} +
+); + +/** Mirrors a topic row in the following accordion: dot, name, trailing value. */ +export const SkeletonTopicRows = ({ count = 3 }: Props) => ( + + {Array.from({ length: count }).map((_, index) => ( +
+
+ + +
+ +
+ ))} +
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonPageHeader.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonPageHeader.tsx new file mode 100644 index 000000000..e1620c746 --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonPageHeader.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from '@components/Skeleton'; + +type Props = { + /** Mirrors `PageHeader`, which drops the paragraph when there is none. */ + description?: boolean; + action?: boolean; +}; + +/** Matches the shape of `PageHeader`. */ +export const SkeletonPageHeader = ({ description = true, action = false }: Props) => ( +
+
+ + {description && } +
+ {action && } +
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonScreen.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonScreen.tsx new file mode 100644 index 000000000..0d89eafde --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonScreen.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from 'react-i18next'; + +type Props = React.ComponentProps<'div'>; + +/** + * Announces one loading region. + * + * The bars inside are `aria-hidden`, so without this wrapper a screen reader + * hears nothing at all while a page loads. + */ +export const SkeletonScreen = ({ className, children, ...props }: Props) => { + const { t } = useTranslation(); + + return ( +
$.common.loading)} className={className} {...props}> + {children} +
+ ); +}; diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonStatCard.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonStatCard.tsx new file mode 100644 index 000000000..628061997 --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonStatCard.tsx @@ -0,0 +1,20 @@ +import { Card, CardContent } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; +import { cn } from '@utils/shadcn'; + +type Props = { + /** Mirrors the cards that carry a second, smaller line under the value. */ + caption?: boolean; + className?: string; +}; + +/** Matches the small dashboard and stakes cards: label, value, caption. */ +export const SkeletonStatCard = ({ caption = true, className }: Props) => ( + + + + + {caption && } + + +); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonText.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonText.tsx new file mode 100644 index 000000000..60840cb6a --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonText.tsx @@ -0,0 +1,35 @@ +import { Skeleton } from '@components/Skeleton'; +import { cn } from '@utils/shadcn'; + +type Size = 'sm' | 'base' | 'lg'; + +type Props = { + /** Number of bars. The last one is short, the way a paragraph ends. */ + lines?: number; + /** Matches the text scale the bars stand in for. */ + size?: Size; + className?: string; +}; + +const heights: Record = { + sm: 'h-3', + base: 'h-3.5', + lg: 'h-4', +}; + +const gaps: Record = { + sm: 'gap-2', + base: 'gap-2.5', + lg: 'gap-3', +}; + +export const SkeletonText = ({ lines = 3, size = 'base', className }: Props) => ( +
+ {Array.from({ length: lines }).map((_, index) => ( + 1 ? 'w-3/5' : 'w-full')} + /> + ))} +
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/SkeletonTransactionList.tsx b/src/governance-app-frontend/src/common/components/skeletons/SkeletonTransactionList.tsx new file mode 100644 index 000000000..9313e658f --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/SkeletonTransactionList.tsx @@ -0,0 +1,49 @@ +import { Card, CardContent } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { SkeletonScreen } from './SkeletonScreen'; + +type Props = { + count?: number; +}; + +/** Mirrors `AccountTransactionItem`: icon, label, timestamp, amount. */ +export const SkeletonTransactionList = ({ count = 3 }: Props) => ( + + {Array.from({ length: count }).map((_, index) => ( + + +
+ +
+
+ + +
+
+ + +
+
+
+
+
+ ))} +
+); + +/** The compact variant used inside the recent activity card. */ +export const SkeletonTransactionRows = ({ count = 3 }: Props) => ( + + {Array.from({ length: count }).map((_, index) => ( +
+ +
+ + +
+ +
+ ))} +
+); diff --git a/src/governance-app-frontend/src/common/components/skeletons/VotingSkeleton.tsx b/src/governance-app-frontend/src/common/components/skeletons/VotingSkeleton.tsx new file mode 100644 index 000000000..f2a153a08 --- /dev/null +++ b/src/governance-app-frontend/src/common/components/skeletons/VotingSkeleton.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent } from '@components/Card'; +import { Skeleton } from '@components/Skeleton'; + +import { ProposalListSkeleton } from './ProposalListSkeleton'; +import { SkeletonPageHeader } from './SkeletonPageHeader'; + +/** + * Mirrors the `/voting` layout: header, following card, proposals list. + * + * `ProposalListSkeleton` inside announces the load, so this wrapper stays a + * plain `div`. Nested status regions read twice. + */ +export const VotingSkeleton = () => ( +
+ + + {/* FollowedNeuronCard. */} + + +
+ + +
+ +
+
+ +
+ + +
+
+); diff --git a/src/governance-app-frontend/src/common/constants/extra.ts b/src/governance-app-frontend/src/common/constants/extra.ts index 39cd2b133..8425675c4 100644 --- a/src/governance-app-frontend/src/common/constants/extra.ts +++ b/src/governance-app-frontend/src/common/constants/extra.ts @@ -14,10 +14,6 @@ export const PAGINATION_LIMIT_PROPOSALS = 25; // Somehow this one has a different limit in case of a certified request. export const PAGINATION_LIMIT_TRANSACTIONS = 50; -// Loading indicators are held back for this long before appearing, so a fast -// response resolves into content instead of flashing a skeleton on the way. -export const LOADING_INDICATOR_DELAY = 120; - export const E8S_PRECISION = 8; export const E8S = 10 ** E8S_PRECISION; export const E8Sn = BigInt(E8S); diff --git a/src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts b/src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts deleted file mode 100644 index ae1aa6f6f..000000000 --- a/src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { LOADING_INDICATOR_DELAY } from '@constants/extra'; - -/** - * Returns `true` only once `active` has stayed true for `delayMs`. - * - * Used to hold back loading indicators. Queries resolve at whatever speed the - * network allows — a warm IC query call comes back in well under 100ms — and - * painting a skeleton for a handful of frames on the way to content reads as a - * flicker. Delaying the *indicator* keeps that away without delaying the data. - */ -export const useDelayedFlag = (active: boolean, delayMs: number = LOADING_INDICATOR_DELAY) => { - const [elapsed, setElapsed] = useState(false); - const [previousActive, setPreviousActive] = useState(active); - - // Rearm on any change of `active`, so the next active spell waits the delay - // out again. Adjusting during render rather than in an effect: React reruns - // this component before committing, so no extra frame is shown. - if (previousActive !== active) { - setPreviousActive(active); - setElapsed(false); - } - - useEffect(() => { - if (!active) return; - - const timeout = window.setTimeout(() => setElapsed(true), delayMs); - - return () => window.clearTimeout(timeout); - }, [active, delayMs]); - - return active && elapsed; -}; diff --git a/src/governance-app-frontend/src/features/account/components/AccountCard.tsx b/src/governance-app-frontend/src/features/account/components/AccountCard.tsx index ee1fb4cb1..66eb5c252 100644 --- a/src/governance-app-frontend/src/features/account/components/AccountCard.tsx +++ b/src/governance-app-frontend/src/features/account/components/AccountCard.tsx @@ -77,7 +77,7 @@ export function AccountCard() { )} {balanceQuery.isLoading || tickersQuery.isLoading ? ( - + ) : (

diff --git a/src/governance-app-frontend/src/features/accounts/components/AccountsCard.tsx b/src/governance-app-frontend/src/features/accounts/components/AccountsCard.tsx index dd3e226c1..e4daad7b9 100644 --- a/src/governance-app-frontend/src/features/accounts/components/AccountsCard.tsx +++ b/src/governance-app-frontend/src/features/accounts/components/AccountsCard.tsx @@ -41,7 +41,7 @@ export const AccountsCard = () => { {t(($) => $.accounts.title)}

{isLoading ? ( - + ) : ( {count} )} @@ -59,7 +59,7 @@ export const AccountsCard = () => { )} {isLoading || tickersQuery.isLoading ? ( - + ) : (

@@ -75,8 +75,8 @@ export const AccountsCard = () => {

{Array.from({ length: 2 }).map((_, i) => (
- - + +
))}
diff --git a/src/governance-app-frontend/src/features/accounts/components/AccountsList.tsx b/src/governance-app-frontend/src/features/accounts/components/AccountsList.tsx index b75cc017e..82f4c8110 100644 --- a/src/governance-app-frontend/src/features/accounts/components/AccountsList.tsx +++ b/src/governance-app-frontend/src/features/accounts/components/AccountsList.tsx @@ -1,4 +1,5 @@ -import { Skeleton } from '@components/Skeleton'; +import { SkeletonAccountCard } from '@components/skeletons/SkeletonAccountCard'; +import { SkeletonScreen } from '@components/skeletons/SkeletonScreen'; import { useAccounts } from '../hooks/useAccounts'; import { AccountsListItem } from './AccountsListItem'; @@ -9,10 +10,10 @@ export const AccountsList = () => { if (!accountsState) { return ( -
- - -
+ + + + ); } diff --git a/src/governance-app-frontend/src/features/accounts/components/AccountsListItem.tsx b/src/governance-app-frontend/src/features/accounts/components/AccountsListItem.tsx index 4d1a7f924..f323d0843 100644 --- a/src/governance-app-frontend/src/features/accounts/components/AccountsListItem.tsx +++ b/src/governance-app-frontend/src/features/accounts/components/AccountsListItem.tsx @@ -116,7 +116,7 @@ function AccountBalance({ return (
- +
); } diff --git a/src/governance-app-frontend/src/features/accounts/components/AccountsTotalCard.tsx b/src/governance-app-frontend/src/features/accounts/components/AccountsTotalCard.tsx index 2681d8be5..1286641b4 100644 --- a/src/governance-app-frontend/src/features/accounts/components/AccountsTotalCard.tsx +++ b/src/governance-app-frontend/src/features/accounts/components/AccountsTotalCard.tsx @@ -64,7 +64,7 @@ export const AccountsTotalCard = () => {

)} {isLoading || tickersQuery.isLoading ? ( - + ) : (

@@ -96,8 +96,8 @@ export const AccountsTotalCard = () => { {isLoading ? (

- - + +
) : (
diff --git a/src/governance-app-frontend/src/features/accounts/components/RecentTransactions.tsx b/src/governance-app-frontend/src/features/accounts/components/RecentTransactions.tsx index a2621b2c9..fd2c37379 100644 --- a/src/governance-app-frontend/src/features/accounts/components/RecentTransactions.tsx +++ b/src/governance-app-frontend/src/features/accounts/components/RecentTransactions.tsx @@ -5,7 +5,7 @@ import { txConfig } from '@features/transactions/utils/txConfig'; import { Badge } from '@components/badge'; import { Card, CardContent, CardHeader } from '@components/Card'; import { SensitiveValue } from '@components/SensitiveValue'; -import { Skeleton } from '@components/Skeleton'; +import { SkeletonTransactionRows } from '@components/skeletons/SkeletonTransactionList'; import { E8Sn } from '@constants/extra'; import { bigIntDiv } from '@utils/bigInt'; import { secondsToDate, secondsToTime } from '@utils/date'; @@ -29,16 +29,7 @@ export const RecentTransactions = () => { {isLoading ? ( - Array.from({ length: 3 }).map((_, i) => ( -
- -
- - -
- -
- )) + ) : transactions && transactions.length > 0 ? ( transactions.map((tx) => ) ) : ( diff --git a/src/governance-app-frontend/src/features/addressBook/components/AddressBookModal.tsx b/src/governance-app-frontend/src/features/addressBook/components/AddressBookModal.tsx index 5d823fd06..bb005d4f8 100644 --- a/src/governance-app-frontend/src/features/addressBook/components/AddressBookModal.tsx +++ b/src/governance-app-frontend/src/features/addressBook/components/AddressBookModal.tsx @@ -16,7 +16,7 @@ import { ResponsiveDialogHeader, ResponsiveDialogTitle, } from '@components/ResponsiveDialog'; -import { Spinner } from '@components/Spinner'; +import { SkeletonAddressBookRows } from '@components/skeletons/SkeletonListRows'; import { Tooltip, TooltipContent, TooltipTrigger } from '@components/Tooltip'; import { ADDRESS_BOOK_MAX_ENTRIES } from '@constants/addressBook'; import { useAddressBook } from '@hooks/addressBook/useAddressBook'; @@ -80,16 +80,7 @@ export const AddressBookModal: React.FC = ({ isOpen, onOpenChange }) => { > query={addressBookQuery} isEmpty={(data) => data.response.named_addresses.length === 0} - loadingComponent={ -
-
- -
-

- {t(($) => $.addressBook.sendFlow.tooltipLoading)} -

-
- } + loadingComponent={} emptyComponent={
{ )}

)} - {isRewardEventLoading ? ( - - ) : ( - nonNullish(lastRewardTimestamp) && ( -

- {t(($) => $.home.lastRewardEvent, { - date: secondsToDate(Number(lastRewardTimestamp)), - })} -

- ) - )} +
+ {isRewardEventLoading ? ( + + ) : ( + nonNullish(lastRewardTimestamp) && ( +

+ {t(($) => $.home.lastRewardEvent, { + date: secondsToDate(Number(lastRewardTimestamp)), + })} +

+ ) + )} +
); diff --git a/src/governance-app-frontend/src/features/dashboard/components/IcpPriceCard.tsx b/src/governance-app-frontend/src/features/dashboard/components/IcpPriceCard.tsx index 095d2c036..bb1c8811c 100644 --- a/src/governance-app-frontend/src/features/dashboard/components/IcpPriceCard.tsx +++ b/src/governance-app-frontend/src/features/dashboard/components/IcpPriceCard.tsx @@ -28,25 +28,28 @@ export const IcpPriceCard = () => { {t(($) => $.home.icpPrice)}

{tickersQuery.isLoading ? ( - <> - - - + ) : ( - <> -

- {icpPriceUsd ? `$${icpPriceUsd}` : '—'} -

- {nonNullish(change) && ( +

+ {icpPriceUsd ? `$${icpPriceUsd}` : '—'} +

+ )} + {/* The row keeps its height with or without a change, so the card does + not shrink when the skeleton leaves. */} +
+ {tickersQuery.isLoading ? ( + + ) : ( + nonNullish(change) && (

{isPositive ? '▲' : '▼'} {formatPercentage(Math.abs(change))} ({t(($) => $.home.icpPrice24h)})

- )} - - )} + ) + )} +
); diff --git a/src/governance-app-frontend/src/features/dashboard/components/TotalStakedCard.tsx b/src/governance-app-frontend/src/features/dashboard/components/TotalStakedCard.tsx index bc6adb192..5e9cfdccb 100644 --- a/src/governance-app-frontend/src/features/dashboard/components/TotalStakedCard.tsx +++ b/src/governance-app-frontend/src/features/dashboard/components/TotalStakedCard.tsx @@ -35,32 +35,33 @@ export const TotalStakedCard = () => { {t(($) => $.home.totalStaked)}

{isLoading ? ( - <> - - - + ) : ( - <> -

- {nonNullish(tvl) ? ( - - ) : ( - '—' - )} -

- {nonNullish(stakedPercentage) && ( -

+

+ {nonNullish(tvl) ? ( + + ) : ( + '—' + )} +

+ )} +
+ {isLoading ? ( + + ) : ( + nonNullish(stakedPercentage) && ( +

{t(($) => $.home.ofTotalSupply, { value: formatNumber(stakedPercentage, { minFraction: 1, maxFraction: 1 }), })}

- )} - - )} + ) + )} +
); diff --git a/src/governance-app-frontend/src/features/proposals/components/ProposalDetailsVoting.tsx b/src/governance-app-frontend/src/features/proposals/components/ProposalDetailsVoting.tsx index 4dda0bac9..3d19ec394 100644 --- a/src/governance-app-frontend/src/features/proposals/components/ProposalDetailsVoting.tsx +++ b/src/governance-app-frontend/src/features/proposals/components/ProposalDetailsVoting.tsx @@ -1,11 +1,12 @@ import { ProposalInfo, Vote } from '@icp-sdk/canisters/nns'; import { useInternetIdentity } from 'ic-use-internet-identity'; -import { CheckCircle, Loader2, ThumbsDown, ThumbsUp, TriangleAlert } from 'lucide-react'; +import { CheckCircle, ThumbsDown, ThumbsUp, TriangleAlert } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@components/button'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@components/Card'; +import { Spinner } from '@components/Spinner'; import { E8S } from '@constants/extra'; import { formatNumber, formatPercentage } from '@utils/numbers'; import { cn } from '@utils/shadcn'; @@ -178,7 +179,7 @@ export const ProposalDetailsVoting: React.FC = ({ proposal }) => { variant="outline" > {isVoting && votingFor === Vote.Yes ? ( - + ) : ( )} @@ -197,7 +198,7 @@ export const ProposalDetailsVoting: React.FC = ({ proposal }) => { variant="outline" > {isVoting && votingFor === Vote.No ? ( - + ) : ( )} diff --git a/src/governance-app-frontend/src/features/proposals/components/ProposalListItem.tsx b/src/governance-app-frontend/src/features/proposals/components/ProposalListItem.tsx index 16e65d44a..357725c87 100644 --- a/src/governance-app-frontend/src/features/proposals/components/ProposalListItem.tsx +++ b/src/governance-app-frontend/src/features/proposals/components/ProposalListItem.tsx @@ -3,7 +3,6 @@ import { secondsToDuration } from '@dfinity/utils'; import { CheckCircle, Clock, - Loader2, MessageSquareOff, ShieldBan, Tag, @@ -18,6 +17,7 @@ import { Badge } from '@components/badge'; import { Button } from '@components/button'; import { Card, CardFooter, CardHeader } from '@components/Card'; import { CertifiedBadge } from '@components/CertifiedBadge'; +import { Spinner } from '@components/Spinner'; import { E8S } from '@constants/extra'; import { formatPercentage } from '@utils/numbers'; import { cn } from '@utils/shadcn'; @@ -210,7 +210,7 @@ export function ProposalListItem({ proposal, certified, isSpam, isNonActionable variant="outline" > {isVoting && votingFor === Vote.Yes ? ( - + ) : ( )} @@ -229,7 +229,7 @@ export function ProposalListItem({ proposal, certified, isSpam, isNonActionable variant="outline" > {isVoting && votingFor === Vote.No ? ( - + ) : ( )} diff --git a/src/governance-app-frontend/src/features/stakes/components/FollowingStatusAlert.tsx b/src/governance-app-frontend/src/features/stakes/components/FollowingStatusAlert.tsx index cc49f9d9c..866bf349e 100644 --- a/src/governance-app-frontend/src/features/stakes/components/FollowingStatusAlert.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/FollowingStatusAlert.tsx @@ -1,7 +1,7 @@ import type { NeuronInfo } from '@icp-sdk/canisters/nns'; import { isNullish } from '@dfinity/utils'; import { Link } from '@tanstack/react-router'; -import { AlertTriangle, ArrowRight, Clock, Loader2, TrendingDown } from 'lucide-react'; +import { AlertTriangle, ArrowRight, Clock, TrendingDown } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { AnalyticsEvent } from '@features/analytics/events'; @@ -9,6 +9,7 @@ import { analytics } from '@features/analytics/service'; import { Alert, AlertDescription, AlertTitle } from '@components/Alert'; import { Button } from '@components/button'; +import { Spinner } from '@components/Spinner'; import { useGovernanceEconomics } from '@hooks/governance/useGovernanceEconomics'; import { mapCanisterError } from '@utils/errors'; import { @@ -142,7 +143,7 @@ export function FollowingStatusAlert({ neuron, isHotkey }: Props) { > {isPending ? ( <> - + {t(($) => $.neuron.followingStatus.processing)} ) : ( diff --git a/src/governance-app-frontend/src/features/stakes/components/StakedCard.tsx b/src/governance-app-frontend/src/features/stakes/components/StakedCard.tsx index 7aa33910f..7902b3d8f 100644 --- a/src/governance-app-frontend/src/features/stakes/components/StakedCard.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/StakedCard.tsx @@ -91,7 +91,7 @@ export function StakedCard() { {stakingRewards.apy.cur < stakingRewards.apy.max && } ) : ( - + )}
@@ -108,7 +108,7 @@ export function StakedCard() { )} {neuronsQuery.isLoading || tickersQuery.isLoading ? ( - + ) : (

diff --git a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailDissolveView.tsx b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailDissolveView.tsx index 0cce14495..e3c75cc77 100644 --- a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailDissolveView.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailDissolveView.tsx @@ -1,9 +1,10 @@ import type { NeuronInfo } from '@icp-sdk/canisters/nns'; -import { AlertTriangle, Info, Key, Loader2 } from 'lucide-react'; +import { AlertTriangle, Info, Key } from 'lucide-react'; import { Trans, useTranslation } from 'react-i18next'; import { Alert, AlertDescription } from '@components/Alert'; import { Button } from '@components/button'; +import { Spinner } from '@components/Spinner'; import { mapCanisterError } from '@utils/errors'; import { errorNotification, successNotification } from '@utils/notification'; @@ -124,7 +125,7 @@ export function NeuronDetailDissolveView({ > {isPending ? ( <> - + {isDissolving ? t(($) => $.neuronDetailModal.dissolve.confirmingStop) : t(($) => $.neuronDetailModal.dissolve.confirmingStart)} diff --git a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseDelayView.tsx b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseDelayView.tsx index f57409cf6..37f056cf9 100644 --- a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseDelayView.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseDelayView.tsx @@ -1,11 +1,12 @@ import type { NeuronInfo } from '@icp-sdk/canisters/nns'; -import { AlertTriangle, Key, Loader2 } from 'lucide-react'; +import { AlertTriangle, Key } from 'lucide-react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, AlertDescription } from '@components/Alert'; import { Button } from '@components/button'; import { MaxRewardsBadge } from '@components/MaxRewardsBadge'; +import { Spinner } from '@components/Spinner'; import { mapCanisterError } from '@utils/errors'; import { getNeuronDissolveDelaySeconds, getNeuronIsMaxDissolveDelay } from '@utils/neuron'; import { errorNotification, successNotification } from '@utils/notification'; @@ -169,7 +170,7 @@ export function NeuronDetailIncreaseDelayView({ > {isPending ? ( <> - + {t(($) => $.neuronDetailModal.increaseDelay.confirming)} ) : ( diff --git a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseStakeView.tsx b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseStakeView.tsx index da01f92e9..3d19de354 100644 --- a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseStakeView.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailIncreaseStakeView.tsx @@ -1,6 +1,6 @@ import type { NeuronInfo } from '@icp-sdk/canisters/nns'; import { nonNullish } from '@dfinity/utils'; -import { AlertTriangle, Info, Loader2 } from 'lucide-react'; +import { AlertTriangle, Info } from 'lucide-react'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,6 +12,7 @@ import { Alert, AlertDescription } from '@components/Alert'; import { AmountInput } from '@components/AmountInput'; import { Button } from '@components/button'; import { Label } from '@components/Label'; +import { Spinner } from '@components/Spinner'; import { CANISTER_ID_ICP_LEDGER } from '@constants/canisterIds'; import { E8Sn, ICP_TRANSACTION_FEE } from '@constants/extra'; import { useIcpLedgerAccountBalance } from '@hooks/icpLedger'; @@ -204,7 +205,7 @@ export function NeuronDetailIncreaseStakeView({ neuron, onSuccess, onProcessingC > {isPending ? ( <> - + {t(($) => $.neuronDetailModal.increaseStake.confirming)} ) : ( diff --git a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailMaturityModeView.tsx b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailMaturityModeView.tsx index 6025f1cdb..3cc6601c6 100644 --- a/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailMaturityModeView.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailMaturityModeView.tsx @@ -1,5 +1,5 @@ import type { NeuronInfo } from '@icp-sdk/canisters/nns'; -import { Info, Key, Loader2 } from 'lucide-react'; +import { Info, Key } from 'lucide-react'; import { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -7,6 +7,7 @@ import { Alert, AlertDescription } from '@components/Alert'; import { Button } from '@components/button'; import { MaxRewardsBadge } from '@components/MaxRewardsBadge'; import { SegmentedToggle, type SegmentedToggleValue } from '@components/SegmentedToggle'; +import { Spinner } from '@components/Spinner'; import { mapCanisterError } from '@utils/errors'; import { getNeuronIsAutoStakingMaturity } from '@utils/neuron'; import { errorNotification, successNotification } from '@utils/notification'; @@ -127,7 +128,7 @@ export function NeuronDetailMaturityModeView({ > {isPending ? ( <> - + {t(($) => $.neuronDetailModal.maturityMode.confirming)} ) : ( diff --git a/src/governance-app-frontend/src/features/stakes/components/stakingWizard/StakingWizardStepConfirmation.tsx b/src/governance-app-frontend/src/features/stakes/components/stakingWizard/StakingWizardStepConfirmation.tsx index e946933c7..3c99185e9 100644 --- a/src/governance-app-frontend/src/features/stakes/components/stakingWizard/StakingWizardStepConfirmation.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/stakingWizard/StakingWizardStepConfirmation.tsx @@ -1,9 +1,10 @@ -import { AlertTriangle, Loader } from 'lucide-react'; +import { AlertTriangle } from 'lucide-react'; import { motion } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { AnimatedCheckmark } from '@components/AnimatedCheckmark'; import { Button } from '@components/button'; +import { ProcessingSpinner } from '@components/ProcessingSpinner'; import { STAKING_WIZARD_DISSOLVE_DELAY_OPTIONS } from './constants'; import { StakingWizardCreateNeuronStep, StakingWizardFormState } from './types'; @@ -54,7 +55,7 @@ export function StakingWizardStepConfirmation({ className="flex flex-col items-center gap-6 text-center" data-testid="staking-wizard-processing" > - +

{t(($) => $.stakeWizardModal.steps.confirmation.processing.title)} @@ -161,19 +162,6 @@ export function StakingWizardStepConfirmation({ ); } -function AnimatedLoader() { - return ( - - - - ); -} - function AnimatedErrorIcon() { return ( {t(($) => $.neuron.summary.capital)}

- {tickersQuery.isLoading ? ( - <> - - - - ) : ( - <> -

- - {t(($) => $.common.inIcp, { value: formatNumber(totalStaked) })} - -

-

+ {/* The staked total arrives with the neurons, so it never waits on the + ticker price. Only the USD line below does. */} +

+ + {t(($) => $.common.inIcp, { value: formatNumber(totalStaked) })} + +

+
+ {tickersQuery.isLoading ? ( + + ) : ( +

{nonNullish(usdValue) ? ( {t(($) => $.account.approxUsd, { @@ -53,8 +52,8 @@ export function CapitalCard({ neurons }: CapitalCardProps) { '—' )}

- - )} + )} +
); diff --git a/src/governance-app-frontend/src/features/stakes/components/summaryCards/EarningsCard.tsx b/src/governance-app-frontend/src/features/stakes/components/summaryCards/EarningsCard.tsx index 629f00638..b85a1bfed 100644 --- a/src/governance-app-frontend/src/features/stakes/components/summaryCards/EarningsCard.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/summaryCards/EarningsCard.tsx @@ -53,7 +53,7 @@ export function EarningsCard({ neurons }: EarningsCardProps) { ) : stakingRewardsError ? (

—

) : ( - + )}
diff --git a/src/governance-app-frontend/src/features/stakes/components/summaryCards/EngagementCard.tsx b/src/governance-app-frontend/src/features/stakes/components/summaryCards/EngagementCard.tsx index fb3262c24..363f48929 100644 --- a/src/governance-app-frontend/src/features/stakes/components/summaryCards/EngagementCard.tsx +++ b/src/governance-app-frontend/src/features/stakes/components/summaryCards/EngagementCard.tsx @@ -38,8 +38,8 @@ export function EngagementCard({ neurons }: EngagementCardProps) { {isLoading ? ( <> - - + + ) : ( <> diff --git a/src/governance-app-frontend/src/features/transactions/components/TransactionListDialog.tsx b/src/governance-app-frontend/src/features/transactions/components/TransactionListDialog.tsx index 9482e57eb..48f2cd080 100644 --- a/src/governance-app-frontend/src/features/transactions/components/TransactionListDialog.tsx +++ b/src/governance-app-frontend/src/features/transactions/components/TransactionListDialog.tsx @@ -8,7 +8,6 @@ import { useNeuronAccountsIds } from '@features/account/hooks/useNeuronAccountsI import { buildTrustedAddresses } from '@features/account/utils/addressPoisoning'; import { useAccounts } from '@features/accounts/hooks/useAccounts'; -import { MultipleSkeletons } from '@components/MultipleSkeletons'; import { QueryStates } from '@components/QueryStates'; import { ResponsiveDialog, @@ -17,6 +16,7 @@ import { ResponsiveDialogHeader, ResponsiveDialogTitle, } from '@components/ResponsiveDialog'; +import { SkeletonTransactionList } from '@components/skeletons/SkeletonTransactionList'; import { useAddressBook } from '@hooks/addressBook/useAddressBook'; import { useIcpIndexTransactions } from '@hooks/icpIndex/useIcpIndexTransactions'; import { CertifiedData } from '@typings/queries'; @@ -93,15 +93,13 @@ export function TransactionListDialog({ {isNullish(accountIdHex) ? ( -
- -
+ ) : (
> infiniteQuery={transactions} isEmpty={(data) => !data.pages?.length || !data.pages[0].response.transactions.length} - loadingComponent={} + loadingComponent={} emptyComponent={

{t(($) => $.account.noTransactions)} diff --git a/src/governance-app-frontend/src/features/voting/components/AdvancedFollowingModal.tsx b/src/governance-app-frontend/src/features/voting/components/AdvancedFollowingModal.tsx index ebd07490e..ccba35203 100644 --- a/src/governance-app-frontend/src/features/voting/components/AdvancedFollowingModal.tsx +++ b/src/governance-app-frontend/src/features/voting/components/AdvancedFollowingModal.tsx @@ -1,6 +1,6 @@ import { type NeuronInfo, Topic } from '@icp-sdk/canisters/nns'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { AlertTriangle, Loader2, Plus } from 'lucide-react'; +import { AlertTriangle, Plus } from 'lucide-react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,7 +16,8 @@ import { ResponsiveDialogHeader, ResponsiveDialogTitle, } from '@components/ResponsiveDialog'; -import { Skeleton } from '@components/Skeleton'; +import { SkeletonTopicRows } from '@components/skeletons/SkeletonListRows'; +import { Spinner } from '@components/Spinner'; import { useGovernanceNeurons, useNnsGovernance } from '@hooks/governance'; import { useGovernanceKnownNeurons } from '@hooks/governance/useGovernanceKnownNeurons'; import { errorMessage } from '@utils/error'; @@ -77,17 +78,7 @@ export function AdvancedFollowingModal({ open, onOpenChange }: Props) { {isLoading ? ( -

- {[1, 2, 3].map((i) => ( -
-
- - -
- -
- ))} -
+ ) : ( <>
@@ -101,11 +92,7 @@ export function AdvancedFollowingModal({ open, onOpenChange }: Props) { }} data-testid="set-followees-btn" > - {isWaitingForCertifiedData ? ( - - ) : ( - - )} + {isWaitingForCertifiedData ? : } {isWaitingForCertifiedData ? t(($) => $.voting.manageFollowing.certifying) : t(($) => $.voting.manageFollowing.setFollowees)} diff --git a/src/governance-app-frontend/src/features/voting/components/FollowedNeuronCard.tsx b/src/governance-app-frontend/src/features/voting/components/FollowedNeuronCard.tsx index debd41496..6af583902 100644 --- a/src/governance-app-frontend/src/features/voting/components/FollowedNeuronCard.tsx +++ b/src/governance-app-frontend/src/features/voting/components/FollowedNeuronCard.tsx @@ -72,7 +72,7 @@ export const FollowedNeuronCard = ({ neuron }: Props) => {
- {knownNeuronsQuery.isLoading ? : renderContent()} + {knownNeuronsQuery.isLoading ? : renderContent()}