mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: show one row of themes with a show all button
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ThemesSettings } from '../themes-settings';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
|
||||
// The gallery shows one row of cards until "Show all", keeps the active theme
|
||||
// in that row, and opens when a search result points at a hidden theme.
|
||||
// vitest.setup.ts renders labels as their raw keys; the built-in themes are
|
||||
// the store's own.
|
||||
|
||||
function cardNames(): string[] {
|
||||
// Cards only: the embedded wrapper carries the section's own search label.
|
||||
return Array.from(document.querySelectorAll('[data-search-label].relative'))
|
||||
.map((el) => el.getAttribute('data-search-label') ?? '');
|
||||
}
|
||||
|
||||
describe('ThemesSettings collapse', () => {
|
||||
beforeEach(() => {
|
||||
act(() => { useThemeStore.setState({ activeThemeId: null }); });
|
||||
});
|
||||
|
||||
it('shows three cards and a Show all button', () => {
|
||||
render(<ThemesSettings embedded />);
|
||||
const total = useThemeStore.getState().installedThemes.length + 1;
|
||||
expect(total).toBeGreaterThan(3);
|
||||
expect(cardNames()).toHaveLength(3);
|
||||
expect(cardNames()[0]).toBe('default_name');
|
||||
expect(cardNames()).not.toContain('title');
|
||||
const button = screen.getByTestId('themes-show-all');
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(button);
|
||||
expect(cardNames()).toHaveLength(total);
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true');
|
||||
fireEvent.click(button);
|
||||
expect(cardNames()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('keeps the active theme in the collapsed row', () => {
|
||||
const last = useThemeStore.getState().installedThemes.at(-1)!;
|
||||
act(() => { useThemeStore.setState({ activeThemeId: last.id }); });
|
||||
render(<ThemesSettings embedded />);
|
||||
expect(cardNames()).toHaveLength(3);
|
||||
expect(cardNames()).toContain(last.name);
|
||||
});
|
||||
|
||||
it('opens for a search result that names a hidden theme', () => {
|
||||
const last = useThemeStore.getState().installedThemes.at(-1)!;
|
||||
render(<ThemesSettings embedded />);
|
||||
expect(cardNames()).not.toContain(last.name);
|
||||
act(() => { window.dispatchEvent(new CustomEvent('settings-reveal', { detail: { label: last.name } })); });
|
||||
expect(cardNames()).toContain(last.name);
|
||||
});
|
||||
});
|
||||
@@ -516,6 +516,8 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
|
||||
new CustomEvent('settings-plugin-expand', { detail: { pluginId: pendingHighlight.pluginId } })
|
||||
);
|
||||
}
|
||||
// Collapsed lists (the theme gallery) open when the result is inside them.
|
||||
window.dispatchEvent(new CustomEvent('settings-reveal', { detail: { label: pendingHighlight.label } }));
|
||||
|
||||
let cancelled = false;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
@@ -5,7 +5,11 @@ import { useTranslations } from 'next-intl';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
import { SettingsSection } from './settings-section';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Check, Lock } from '@/components/icons';
|
||||
import { Check, Lock, ChevronDown, ChevronUp } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/** Cards shown before "Show all": one row of the grid. */
|
||||
const COLLAPSED_THEME_COUNT = 3;
|
||||
import { toast } from '@/stores/toast-store';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
|
||||
@@ -38,6 +42,27 @@ export function ThemesSettings({ embedded }: { embedded?: boolean } = {}) {
|
||||
theme => !isThemeDisabled(theme.id, !!theme.builtIn)
|
||||
);
|
||||
|
||||
// One row of cards until "Show all": the gallery is long and most people
|
||||
// never change the theme. The active theme always stays in that row.
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const cards: (typeof visibleThemes[number] | null)[] = [null, ...visibleThemes];
|
||||
const activeIndex = Math.max(0, cards.findIndex((theme) => (theme?.id ?? null) === activeThemeId));
|
||||
const collapsed = !expanded && cards.length > COLLAPSED_THEME_COUNT;
|
||||
const shownCards = collapsed
|
||||
? (activeIndex < COLLAPSED_THEME_COUNT
|
||||
? cards.slice(0, COLLAPSED_THEME_COUNT)
|
||||
: [...cards.slice(0, COLLAPSED_THEME_COUNT - 1), cards[activeIndex]])
|
||||
: cards;
|
||||
// A search result for a hidden theme asks the gallery to open first.
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const label = (e as CustomEvent<{ label?: string }>).detail?.label;
|
||||
if (label && visibleThemes.some((theme) => theme.name === label)) setExpanded(true);
|
||||
};
|
||||
window.addEventListener('settings-reveal', handler);
|
||||
return () => window.removeEventListener('settings-reveal', handler);
|
||||
}, [visibleThemes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (forcedThemeId && activeThemeId !== forcedThemeId) {
|
||||
activateTheme(forcedThemeId);
|
||||
@@ -78,22 +103,24 @@ export function ThemesSettings({ embedded }: { embedded?: boolean } = {}) {
|
||||
|
||||
{/* Theme Grid */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{/* Default theme card */}
|
||||
<ThemeCard
|
||||
name={t('default_name')}
|
||||
author="Bulwark"
|
||||
isDefaultTheme
|
||||
variants={['light', 'dark']}
|
||||
isDark={isDark}
|
||||
isActive={activeThemeId === null}
|
||||
isBuiltIn
|
||||
isDefault={!themePolicy.defaultThemeId}
|
||||
disabled={Boolean(forcedThemeId)}
|
||||
onActivate={() => handleActivate(null)}
|
||||
/>
|
||||
|
||||
{/* Installed themes */}
|
||||
{visibleThemes.map(theme => {
|
||||
{shownCards.map((theme) => {
|
||||
if (!theme) {
|
||||
return (
|
||||
<ThemeCard
|
||||
key="default"
|
||||
name={t('default_name')}
|
||||
author="Bulwark"
|
||||
isDefaultTheme
|
||||
variants={['light', 'dark']}
|
||||
isDark={isDark}
|
||||
isActive={activeThemeId === null}
|
||||
isBuiltIn
|
||||
isDefault={!themePolicy.defaultThemeId}
|
||||
disabled={Boolean(forcedThemeId)}
|
||||
onActivate={() => handleActivate(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const isForceEnabled = theme.id === forcedThemeId || theme.forceEnabled || isThemeForceEnabled(theme.id);
|
||||
return (
|
||||
<ThemeCard
|
||||
@@ -114,6 +141,20 @@ export function ThemesSettings({ embedded }: { embedded?: boolean } = {}) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{cards.length > COLLAPSED_THEME_COUNT && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-3 text-xs"
|
||||
aria-expanded={!collapsed}
|
||||
data-testid="themes-show-all"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
>
|
||||
{collapsed ? <ChevronDown className="w-3.5 h-3.5 me-1" /> : <ChevronUp className="w-3.5 h-3.5 me-1" />}
|
||||
{collapsed ? t('show_all', { count: cards.length }) : t('show_fewer')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "تمت استعادة السمة الافتراضية",
|
||||
"default_name": "الافتراضية",
|
||||
"default_badge": "افتراضية",
|
||||
"admin_enforced": "مفروضة من المسؤول"
|
||||
"admin_enforced": "مفروضة من المسؤول",
|
||||
"show_all": "إظهار كل السمات ({count})",
|
||||
"show_fewer": "إظهار أقل"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "التطبيقات",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "S'ha restaurat el tema predeterminat",
|
||||
"default_name": "Predeterminat",
|
||||
"default_badge": "Predeterminat",
|
||||
"admin_enforced": "Imposat per l'administrador"
|
||||
"admin_enforced": "Imposat per l'administrador",
|
||||
"show_all": "Mostra els {count} temes",
|
||||
"show_fewer": "Mostra'n menys"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplicacions",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Výchozí motiv obnoven",
|
||||
"default_name": "Výchozí",
|
||||
"default_badge": "Výchozí",
|
||||
"admin_enforced": "Vynuceno správcem"
|
||||
"admin_enforced": "Vynuceno správcem",
|
||||
"show_all": "Zobrazit všechny motivy ({count})",
|
||||
"show_fewer": "Zobrazit méně"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplikace",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Standardtema gendannet",
|
||||
"default_name": "Standard",
|
||||
"default_badge": "Standard",
|
||||
"admin_enforced": "Gennemtvunget af administrator"
|
||||
"admin_enforced": "Gennemtvunget af administrator",
|
||||
"show_all": "Vis alle {count} temaer",
|
||||
"show_fewer": "Vis færre"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Apps",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Standarddesign wiederhergestellt",
|
||||
"default_name": "Standard",
|
||||
"default_badge": "Standard",
|
||||
"admin_enforced": "Vom Administrator erzwungen"
|
||||
"admin_enforced": "Vom Administrator erzwungen",
|
||||
"show_all": "Alle {count} Themes anzeigen",
|
||||
"show_fewer": "Weniger anzeigen"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Apps",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Default theme restored",
|
||||
"default_name": "Default",
|
||||
"default_badge": "Default",
|
||||
"admin_enforced": "Admin enforced"
|
||||
"admin_enforced": "Admin enforced",
|
||||
"show_all": "Show all {count} themes",
|
||||
"show_fewer": "Show fewer"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Apps",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Tema predeterminado restaurado",
|
||||
"default_name": "Predeterminado",
|
||||
"default_badge": "Predeterminado",
|
||||
"admin_enforced": "Impuesto por el administrador"
|
||||
"admin_enforced": "Impuesto por el administrador",
|
||||
"show_all": "Mostrar los {count} temas",
|
||||
"show_fewer": "Mostrar menos"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplicaciones",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Vaiketeema taastatud",
|
||||
"default_name": "Vaikimisi",
|
||||
"default_badge": "Vaikimisi",
|
||||
"admin_enforced": "Administraatori määratud"
|
||||
"admin_enforced": "Administraatori määratud",
|
||||
"show_all": "Kuva kõik {count} teemat",
|
||||
"show_fewer": "Kuva vähem"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Rakendused",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "پوسته پیشفرض بازیابی شد",
|
||||
"default_name": "پیشفرض",
|
||||
"default_badge": "پیشفرض",
|
||||
"admin_enforced": "اجباری از سوی مدیر"
|
||||
"admin_enforced": "اجباری از سوی مدیر",
|
||||
"show_all": "نمایش همه {count} پوسته",
|
||||
"show_fewer": "نمایش کمتر"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "برنامهها",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Thème par défaut restauré",
|
||||
"default_name": "Par défaut",
|
||||
"default_badge": "Par défaut",
|
||||
"admin_enforced": "Imposé par l'administrateur"
|
||||
"admin_enforced": "Imposé par l'administrateur",
|
||||
"show_all": "Afficher les {count} thèmes",
|
||||
"show_fewer": "Afficher moins"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Applications",
|
||||
|
||||
@@ -1012,7 +1012,9 @@
|
||||
"default_restored": "ערכת הנושא המוגדרת כברירת מחדל שוחזרה",
|
||||
"default_name": "ברירת מחדל",
|
||||
"default_badge": "ברירת מחדל",
|
||||
"admin_enforced": "נאכף על ידי המנהל"
|
||||
"admin_enforced": "נאכף על ידי המנהל",
|
||||
"show_all": "הצגת כל {count} ערכות הנושא",
|
||||
"show_fewer": "הצגת פחות"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "אפליקציות",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Alapértelmezett téma visszaállítva",
|
||||
"default_name": "Alapértelmezett",
|
||||
"default_badge": "Alapértelmezett",
|
||||
"admin_enforced": "Rendszergazda által előírva"
|
||||
"admin_enforced": "Rendszergazda által előírva",
|
||||
"show_all": "Mind a(z) {count} téma megjelenítése",
|
||||
"show_fewer": "Kevesebb megjelenítése"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Alkalmazások",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Tema predefinito ripristinato",
|
||||
"default_name": "Predefinito",
|
||||
"default_badge": "Predefinito",
|
||||
"admin_enforced": "Imposto dall'amministratore"
|
||||
"admin_enforced": "Imposto dall'amministratore",
|
||||
"show_all": "Mostra tutti i {count} temi",
|
||||
"show_fewer": "Mostra meno"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Applicazioni",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "デフォルトのテーマに戻しました",
|
||||
"default_name": "デフォルト",
|
||||
"default_badge": "デフォルト",
|
||||
"admin_enforced": "管理者により固定"
|
||||
"admin_enforced": "管理者により固定",
|
||||
"show_all": "すべてのテーマ({count}件)を表示",
|
||||
"show_fewer": "表示を減らす"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "アプリ",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "기본 테마로 복원되었습니다",
|
||||
"default_name": "기본",
|
||||
"default_badge": "기본",
|
||||
"admin_enforced": "관리자가 지정함"
|
||||
"admin_enforced": "관리자가 지정함",
|
||||
"show_all": "테마 {count}개 모두 표시",
|
||||
"show_fewer": "간략히 보기"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "앱",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Noklusējuma tēma atjaunota",
|
||||
"default_name": "Noklusējuma",
|
||||
"default_badge": "Noklusējuma",
|
||||
"admin_enforced": "Noteicis administrators"
|
||||
"admin_enforced": "Noteicis administrators",
|
||||
"show_all": "Rādīt visas {count} tēmas",
|
||||
"show_fewer": "Rādīt mazāk"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Lietotnes",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Үндсэн сэдвийг сэргээлээ",
|
||||
"default_name": "Үндсэн",
|
||||
"default_badge": "Үндсэн",
|
||||
"admin_enforced": "Администратор тогтоосон"
|
||||
"admin_enforced": "Администратор тогтоосон",
|
||||
"show_all": "Бүх {count} загварыг харуулах",
|
||||
"show_fewer": "Цөөнийг харуулах"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Програмууд",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Standardtema gjenopprettet",
|
||||
"default_name": "Standard",
|
||||
"default_badge": "Standard",
|
||||
"admin_enforced": "Påtvunget av administrator"
|
||||
"admin_enforced": "Påtvunget av administrator",
|
||||
"show_all": "Vis alle {count} temaer",
|
||||
"show_fewer": "Vis færre"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Apper",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Standaardthema hersteld",
|
||||
"default_name": "Standaard",
|
||||
"default_badge": "Standaard",
|
||||
"admin_enforced": "Afgedwongen door beheerder"
|
||||
"admin_enforced": "Afgedwongen door beheerder",
|
||||
"show_all": "Alle {count} thema's tonen",
|
||||
"show_fewer": "Minder tonen"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Apps",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Przywrócono domyślny motyw",
|
||||
"default_name": "Domyślny",
|
||||
"default_badge": "Domyślny",
|
||||
"admin_enforced": "Wymuszony przez administratora"
|
||||
"admin_enforced": "Wymuszony przez administratora",
|
||||
"show_all": "Pokaż wszystkie motywy ({count})",
|
||||
"show_fewer": "Pokaż mniej"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplikacje",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Tema padrão restaurado",
|
||||
"default_name": "Padrão",
|
||||
"default_badge": "Padrão",
|
||||
"admin_enforced": "Imposto pelo administrador"
|
||||
"admin_enforced": "Imposto pelo administrador",
|
||||
"show_all": "Mostrar todos os {count} temas",
|
||||
"show_fewer": "Mostrar menos"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplicativos",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Tema implicită a fost restabilită",
|
||||
"default_name": "Implicită",
|
||||
"default_badge": "Implicită",
|
||||
"admin_enforced": "Impusă de administrator"
|
||||
"admin_enforced": "Impusă de administrator",
|
||||
"show_all": "Afișează toate cele {count} teme",
|
||||
"show_fewer": "Afișează mai puține"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplicații",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Тема по умолчанию восстановлена",
|
||||
"default_name": "По умолчанию",
|
||||
"default_badge": "По умолчанию",
|
||||
"admin_enforced": "Задано администратором"
|
||||
"admin_enforced": "Задано администратором",
|
||||
"show_all": "Показать все темы ({count})",
|
||||
"show_fewer": "Показать меньше"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Приложения",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "Predvolený motív bol obnovený",
|
||||
"default_name": "Predvolený",
|
||||
"default_badge": "Predvolený",
|
||||
"admin_enforced": "Vynútené správcom"
|
||||
"admin_enforced": "Vynútené správcom",
|
||||
"show_all": "Zobraziť všetky motívy ({count})",
|
||||
"show_fewer": "Zobraziť menej"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Aplikácie",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Varsayılan tema geri yüklendi",
|
||||
"default_name": "Varsayılan",
|
||||
"default_badge": "Varsayılan",
|
||||
"admin_enforced": "Yönetici tarafından zorunlu"
|
||||
"admin_enforced": "Yönetici tarafından zorunlu",
|
||||
"show_all": "Tüm {count} temayı göster",
|
||||
"show_fewer": "Daha az göster"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "Uygulamalar",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "Тему за замовчуванням відновлено",
|
||||
"default_name": "За замовчуванням",
|
||||
"default_badge": "За замовчуванням",
|
||||
"admin_enforced": "Встановлено адміністратором"
|
||||
"admin_enforced": "Встановлено адміністратором",
|
||||
"show_all": "Показати всі теми ({count})",
|
||||
"show_fewer": "Показати менше"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "програми",
|
||||
|
||||
@@ -1050,7 +1050,9 @@
|
||||
"default_restored": "已還原預設主題",
|
||||
"default_name": "預設",
|
||||
"default_badge": "預設",
|
||||
"admin_enforced": "管理員強制"
|
||||
"admin_enforced": "管理員強制",
|
||||
"show_all": "顯示全部 {count} 個主題",
|
||||
"show_fewer": "收合"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "應用程式",
|
||||
|
||||
@@ -1047,7 +1047,9 @@
|
||||
"default_restored": "已恢复默认主题",
|
||||
"default_name": "默认",
|
||||
"default_badge": "默认",
|
||||
"admin_enforced": "管理员强制"
|
||||
"admin_enforced": "管理员强制",
|
||||
"show_all": "显示全部 {count} 个主题",
|
||||
"show_fewer": "收起"
|
||||
},
|
||||
"tab_groups": {
|
||||
"apps": "应用",
|
||||
|
||||
Reference in New Issue
Block a user