fix(themes): offline banner for the Theme Store (#1013)

* fix(themes): show an offline banner when the Theme Store is unavailable

When the registry can't be fetched and no catalogue is cached, the store
now shows a clear offline banner (icon, message, retry) in place of the
list, and hides the search/filter toolbar — there is nothing to browse.
The cached-catalogue fallback with its offline indicator is unchanged, so
the store still works offline when a catalogue was previously cached.

* docs(themes): note the offline-banner PR in the Theme Store entry

Add PR #1013 to the still-unreleased Theme Store changelog and credits
entry, alongside the other follow-ups.
This commit is contained in:
Psychotoxical
2026-06-07 11:48:23 +02:00
committed by GitHub
parent b3573e7107
commit 10fc489ce9
13 changed files with 48 additions and 7 deletions
@@ -143,4 +143,14 @@ describe('ThemeStoreSection — pagination & refresh', () => {
await waitFor(() => expect(container.querySelector('.animate-spin')).toBeNull());
expect(rows(container)).toHaveLength(12);
});
it('shows an offline banner and hides the toolbar when the registry is unavailable', async () => {
fetchRegistryMock.mockRejectedValue(new Error('offline'));
renderWithProviders(<ThemeStoreSection />);
expect(await screen.findByText('The Theme Store is offline')).toBeInTheDocument();
// No catalogue to browse → the search/filter toolbar is not rendered.
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
});
});
+27 -5
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2 } from 'lucide-react';
import { Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, WifiOff } from 'lucide-react';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import CoverLightbox from '../CoverLightbox';
import { useThemeStore } from '../../store/themeStore';
@@ -159,7 +159,9 @@ export function ThemeStoreSection() {
</button>
</div>
{/* Toolbar: search + mode filter + refresh */}
{/* Toolbar: search + mode filter + refresh. Hidden when offline with no
catalogue to browse — the offline banner below stands in for it. */}
{!error && (
<div ref={topRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center', marginBottom: '1rem', scrollMarginTop: 8 }}>
<input
type="search"
@@ -195,6 +197,7 @@ export function ThemeStoreSection() {
<RefreshCw size={15} className={refreshing ? 'animate-spin' : ''} />
</button>
</div>
)}
{!loading && stale && (
<div className="settings-hint settings-hint-info" role="status" style={{ marginBottom: '0.75rem' }}>
@@ -207,9 +210,28 @@ export function ThemeStoreSection() {
)}
{!loading && error && (
<div role="alert" style={{ fontSize: 13, color: 'var(--text-muted)' }}>
<p style={{ marginBottom: 8 }}>{t('settings.themeStoreError')}</p>
<button className="btn btn-ghost" onClick={() => load(true)}>{t('settings.themeStoreRetry')}</button>
<div
role="alert"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
gap: 12,
padding: '28px 16px',
border: '1px dashed var(--border)',
borderRadius: 'var(--radius-md, 10px)',
background: 'var(--bg-elevated)',
}}
>
<WifiOff size={28} style={{ color: 'var(--text-muted)' }} aria-hidden="true" />
<div>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>{t('settings.themeStoreOfflineTitle')}</div>
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>{t('settings.themeStoreError')}</div>
</div>
<button className="btn btn-ghost" onClick={() => load(true)} disabled={refreshing}>
{t('settings.themeStoreRetry')}
</button>
</div>
)}