From 31f4a4b4b3633057b3985596ccbc042b56bf46cf Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:49:24 +0200 Subject: [PATCH] feat: show one row of themes with a show all button --- .../themes-settings-collapse.test.tsx | 53 +++++++++++++ components/settings/settings-app.tsx | 2 + components/settings/themes-settings.tsx | 75 ++++++++++++++----- locales/ar/common.json | 4 +- locales/ca/common.json | 4 +- locales/cs/common.json | 4 +- locales/da/common.json | 4 +- locales/de/common.json | 4 +- locales/en/common.json | 4 +- locales/es/common.json | 4 +- locales/et/common.json | 4 +- locales/fa/common.json | 4 +- locales/fr/common.json | 4 +- locales/he/common.json | 4 +- locales/hu/common.json | 4 +- locales/it/common.json | 4 +- locales/ja/common.json | 4 +- locales/ko/common.json | 4 +- locales/lv/common.json | 4 +- locales/mn/common.json | 4 +- locales/nb/common.json | 4 +- locales/nl/common.json | 4 +- locales/pl/common.json | 4 +- locales/pt/common.json | 4 +- locales/ro/common.json | 4 +- locales/ru/common.json | 4 +- locales/sk/common.json | 4 +- locales/tr/common.json | 4 +- locales/uk/common.json | 4 +- locales/zh-TW/common.json | 4 +- locales/zh/common.json | 4 +- 31 files changed, 197 insertions(+), 45 deletions(-) create mode 100644 components/settings/__tests__/themes-settings-collapse.test.tsx diff --git a/components/settings/__tests__/themes-settings-collapse.test.tsx b/components/settings/__tests__/themes-settings-collapse.test.tsx new file mode 100644 index 000000000..ceac48da8 --- /dev/null +++ b/components/settings/__tests__/themes-settings-collapse.test.tsx @@ -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(); + 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(); + 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(); + expect(cardNames()).not.toContain(last.name); + act(() => { window.dispatchEvent(new CustomEvent('settings-reveal', { detail: { label: last.name } })); }); + expect(cardNames()).toContain(last.name); + }); +}); diff --git a/components/settings/settings-app.tsx b/components/settings/settings-app.tsx index 23581bcce..b247c4187 100644 --- a/components/settings/settings-app.tsx +++ b/components/settings/settings-app.tsx @@ -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 | undefined; diff --git a/components/settings/themes-settings.tsx b/components/settings/themes-settings.tsx index 4f5802d64..3978072db 100644 --- a/components/settings/themes-settings.tsx +++ b/components/settings/themes-settings.tsx @@ -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 */}
- {/* Default theme card */} - handleActivate(null)} - /> - - {/* Installed themes */} - {visibleThemes.map(theme => { + {shownCards.map((theme) => { + if (!theme) { + return ( + handleActivate(null)} + /> + ); + } const isForceEnabled = theme.id === forcedThemeId || theme.forceEnabled || isThemeForceEnabled(theme.id); return ( + + {cards.length > COLLAPSED_THEME_COUNT && ( + + )} ); diff --git a/locales/ar/common.json b/locales/ar/common.json index a12f71251..cb0942392 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -1050,7 +1050,9 @@ "default_restored": "تمت استعادة السمة الافتراضية", "default_name": "الافتراضية", "default_badge": "افتراضية", - "admin_enforced": "مفروضة من المسؤول" + "admin_enforced": "مفروضة من المسؤول", + "show_all": "إظهار كل السمات ({count})", + "show_fewer": "إظهار أقل" }, "tab_groups": { "apps": "التطبيقات", diff --git a/locales/ca/common.json b/locales/ca/common.json index 9cdbcc9b9..5c7a55082 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -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", diff --git a/locales/cs/common.json b/locales/cs/common.json index 2e79a1d8b..44b45ecae 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -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", diff --git a/locales/da/common.json b/locales/da/common.json index 5bd284a99..41f7e97b8 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -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", diff --git a/locales/de/common.json b/locales/de/common.json index c3f38c367..4ecec63f7 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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", diff --git a/locales/en/common.json b/locales/en/common.json index ae3360392..10cf32a0c 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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", diff --git a/locales/es/common.json b/locales/es/common.json index 60e5ab2ca..15bb190c0 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -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", diff --git a/locales/et/common.json b/locales/et/common.json index a208f4510..e21d79c43 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -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", diff --git a/locales/fa/common.json b/locales/fa/common.json index 3a11d9b7f..d5fff848a 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -1050,7 +1050,9 @@ "default_restored": "پوسته پیش‌فرض بازیابی شد", "default_name": "پیش‌فرض", "default_badge": "پیش‌فرض", - "admin_enforced": "اجباری از سوی مدیر" + "admin_enforced": "اجباری از سوی مدیر", + "show_all": "نمایش همه {count} پوسته", + "show_fewer": "نمایش کمتر" }, "tab_groups": { "apps": "برنامه‌ها", diff --git a/locales/fr/common.json b/locales/fr/common.json index 5fe565386..324138154 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -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", diff --git a/locales/he/common.json b/locales/he/common.json index 935e51a7b..47f545c9c 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -1012,7 +1012,9 @@ "default_restored": "ערכת הנושא המוגדרת כברירת מחדל שוחזרה", "default_name": "ברירת מחדל", "default_badge": "ברירת מחדל", - "admin_enforced": "נאכף על ידי המנהל" + "admin_enforced": "נאכף על ידי המנהל", + "show_all": "הצגת כל {count} ערכות הנושא", + "show_fewer": "הצגת פחות" }, "tab_groups": { "apps": "אפליקציות", diff --git a/locales/hu/common.json b/locales/hu/common.json index 279e495d4..8ce133a5e 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -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", diff --git a/locales/it/common.json b/locales/it/common.json index 2a2e21070..788c02ab1 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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", diff --git a/locales/ja/common.json b/locales/ja/common.json index bdb918a92..4e8e540a1 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1047,7 +1047,9 @@ "default_restored": "デフォルトのテーマに戻しました", "default_name": "デフォルト", "default_badge": "デフォルト", - "admin_enforced": "管理者により固定" + "admin_enforced": "管理者により固定", + "show_all": "すべてのテーマ({count}件)を表示", + "show_fewer": "表示を減らす" }, "tab_groups": { "apps": "アプリ", diff --git a/locales/ko/common.json b/locales/ko/common.json index 262b4f5db..bee1d9dad 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -1047,7 +1047,9 @@ "default_restored": "기본 테마로 복원되었습니다", "default_name": "기본", "default_badge": "기본", - "admin_enforced": "관리자가 지정함" + "admin_enforced": "관리자가 지정함", + "show_all": "테마 {count}개 모두 표시", + "show_fewer": "간략히 보기" }, "tab_groups": { "apps": "앱", diff --git a/locales/lv/common.json b/locales/lv/common.json index 1891fda37..f6f91956c 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -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", diff --git a/locales/mn/common.json b/locales/mn/common.json index 7541668f0..46ef7cea4 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -1050,7 +1050,9 @@ "default_restored": "Үндсэн сэдвийг сэргээлээ", "default_name": "Үндсэн", "default_badge": "Үндсэн", - "admin_enforced": "Администратор тогтоосон" + "admin_enforced": "Администратор тогтоосон", + "show_all": "Бүх {count} загварыг харуулах", + "show_fewer": "Цөөнийг харуулах" }, "tab_groups": { "apps": "Програмууд", diff --git a/locales/nb/common.json b/locales/nb/common.json index 11e17a00c..d9af0116c 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -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", diff --git a/locales/nl/common.json b/locales/nl/common.json index 7e3cf47b5..8457327af 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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", diff --git a/locales/pl/common.json b/locales/pl/common.json index 8a383a4cb..f7e1db1ab 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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", diff --git a/locales/pt/common.json b/locales/pt/common.json index 896d3d271..a1d81c84b 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -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", diff --git a/locales/ro/common.json b/locales/ro/common.json index 6b56ab67c..056c82a02 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -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", diff --git a/locales/ru/common.json b/locales/ru/common.json index 63c679c30..22ce3c77c 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -1047,7 +1047,9 @@ "default_restored": "Тема по умолчанию восстановлена", "default_name": "По умолчанию", "default_badge": "По умолчанию", - "admin_enforced": "Задано администратором" + "admin_enforced": "Задано администратором", + "show_all": "Показать все темы ({count})", + "show_fewer": "Показать меньше" }, "tab_groups": { "apps": "Приложения", diff --git a/locales/sk/common.json b/locales/sk/common.json index 60bcf9b01..36612cf0a 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -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", diff --git a/locales/tr/common.json b/locales/tr/common.json index 835e281e5..86373a63e 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -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", diff --git a/locales/uk/common.json b/locales/uk/common.json index fa093a4e0..aa6765005 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -1047,7 +1047,9 @@ "default_restored": "Тему за замовчуванням відновлено", "default_name": "За замовчуванням", "default_badge": "За замовчуванням", - "admin_enforced": "Встановлено адміністратором" + "admin_enforced": "Встановлено адміністратором", + "show_all": "Показати всі теми ({count})", + "show_fewer": "Показати менше" }, "tab_groups": { "apps": "програми", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 94933f995..f45ac2a38 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -1050,7 +1050,9 @@ "default_restored": "已還原預設主題", "default_name": "預設", "default_badge": "預設", - "admin_enforced": "管理員強制" + "admin_enforced": "管理員強制", + "show_all": "顯示全部 {count} 個主題", + "show_fewer": "收合" }, "tab_groups": { "apps": "應用程式", diff --git a/locales/zh/common.json b/locales/zh/common.json index 2b99ac7fa..146be782f 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -1047,7 +1047,9 @@ "default_restored": "已恢复默认主题", "default_name": "默认", "default_badge": "默认", - "admin_enforced": "管理员强制" + "admin_enforced": "管理员强制", + "show_all": "显示全部 {count} 个主题", + "show_fewer": "收起" }, "tab_groups": { "apps": "应用",