+
+
}
diff --git a/src/components/settings/settingsTabs.ts b/src/components/settings/settingsTabs.ts
index dc89f132..343619b4 100644
--- a/src/components/settings/settingsTabs.ts
+++ b/src/components/settings/settingsTabs.ts
@@ -46,6 +46,7 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' },
{ tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'home page customize sections' },
{ tab: 'personalisation',titleKey: 'settings.queueToolbarTitle', keywords: 'queue toolbar buttons reorder customize shuffle save load' },
+ { tab: 'appearance', titleKey: 'settings.libraryGridMaxColumnsTitle', keywords: 'grid columns album artist playlist cards layout appearance performance scroll paint' },
{ tab: 'library', titleKey: 'settings.randomMixTitle', keywords: 'random mix blacklist genre keywords filter audiobook' },
{ tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' },
{ tab: 'storage', titleKey: 'settings.offlineDirTitle', keywords: 'offline library download directory folder cache' },
diff --git a/src/hooks/useCardGridMetrics.ts b/src/hooks/useCardGridMetrics.ts
index 7a7852c7..24db38d9 100644
--- a/src/hooks/useCardGridMetrics.ts
+++ b/src/hooks/useCardGridMetrics.ts
@@ -1,6 +1,7 @@
import { useLayoutEffect, useState, type RefObject } from 'react';
+import { useAuthStore } from '../store/authStore';
+import { clampLibraryGridMaxColumns } from '../store/authStoreHelpers';
import {
- CARD_GRID_MAX_COLS,
type CardGridRowHeightVariant,
computeCardGridColumnCount,
computeCellWidthPx,
@@ -8,8 +9,8 @@ import {
} from '../utils/cardGridLayout';
/**
- * ResizeObserver-driven column count (max six) and virtual row height estimate
- * from the measured cell width.
+ * ResizeObserver-driven column count (capped by Settings → Library) and
+ * virtual row height estimate from the measured cell width.
*/
export function useCardGridMetrics(
measureRef: RefObject,
@@ -17,9 +18,13 @@ export function useCardGridMetrics(
variant: CardGridRowHeightVariant,
layoutSignal: number,
): { gridCols: number; rowHeightEst: number } {
+ const maxCols = useAuthStore(s => clampLibraryGridMaxColumns(s.libraryGridMaxColumns));
const [gridCols, setGridCols] = useState(4);
const [rowHeightEst, setRowHeightEst] = useState(() =>
- estimateRowHeightPx(computeCellWidthPx(960, CARD_GRID_MAX_COLS), variant),
+ estimateRowHeightPx(
+ computeCellWidthPx(960, clampLibraryGridMaxColumns(useAuthStore.getState().libraryGridMaxColumns)),
+ variant,
+ ),
);
useLayoutEffect(() => {
@@ -28,7 +33,7 @@ export function useCardGridMetrics(
if (!el) return;
const onResize = () => {
const w = el.clientWidth;
- const cols = computeCardGridColumnCount(w);
+ const cols = computeCardGridColumnCount(w, maxCols);
setGridCols(cols);
setRowHeightEst(estimateRowHeightPx(computeCellWidthPx(w, cols), variant));
};
@@ -36,7 +41,7 @@ export function useCardGridMetrics(
const ro = new ResizeObserver(onResize);
ro.observe(el);
return () => ro.disconnect();
- }, [observerEnabled, variant, layoutSignal]);
+ }, [observerEnabled, variant, layoutSignal, maxCols]);
return { gridCols, rowHeightEst };
}
diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts
index 5173f45b..d37391e1 100644
--- a/src/locales/de/settings.ts
+++ b/src/locales/de/settings.ts
@@ -228,6 +228,10 @@ export const settings = {
aboutContributorsLabel: 'Mitwirkende',
showChangelogOnUpdate: "'Was ist neu' bei Update anzeigen",
showChangelogOnUpdateDesc: 'Blendet nach einem Update einen dezenten Changelog-Banner über Now Playing ein. Klick öffnet die Release Notes, X blendet ihn aus.',
+ libraryGridMaxColumnsTitle: 'Kartenraster in der Bibliothek',
+ libraryGridMaxColumnsPerfHint: 'Mehr Spalten bedeuten mehr Kacheln pro Zeile und oft mehr Layout- und Zeichenaufwand — bei sehr großen Bibliotheken oder langsamerer Hardware spürbarer.',
+ libraryGridMaxColumnsRangeLabel: 'Maximale Spalten ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Gilt für Album-, Künstler-, Wiedergabelisten-, Radio-, Offline- und andere Kartenansichten. Weniger Spalten = größere Kacheln und meist weniger CPU-Last.',
randomMixTitle: 'Zufallsmix-Blacklist',
luckyMixMenuTitle: 'Glücks-Mix im Menü anzeigen',
luckyMixMenuDesc: 'Aktiviert Glücks-Mix im "Mix erstellen"-Hub und als separaten Menüeintrag bei getrennter Navigation. Sichtbar nur bei aktiviertem AudioMuse auf dem aktiven Server.',
diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts
index 2dd1fac4..02ddb3f8 100644
--- a/src/locales/en/settings.ts
+++ b/src/locales/en/settings.ts
@@ -231,6 +231,10 @@ export const settings = {
aboutContributorsLabel: 'Contributors',
showChangelogOnUpdate: "Show 'What's New' on update",
showChangelogOnUpdateDesc: "Show a discreet changelog banner above Now Playing after an update. Click opens the release notes; X dismisses it.",
+ libraryGridMaxColumnsTitle: 'Library card grids',
+ libraryGridMaxColumnsPerfHint: 'Higher values pack more tiles per row and can increase layout and painting work—noticeable on very large libraries or slower devices.',
+ libraryGridMaxColumnsRangeLabel: 'Maximum columns ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Applies to album, artist, playlist, radio, offline, and other card-based library views. Lower values use larger tiles and are usually easier on the CPU.',
randomMixTitle: 'Random Mix Blacklist',
luckyMixMenuTitle: 'Show Lucky Mix in menu',
luckyMixMenuDesc: 'Enables Lucky Mix in Build a Mix and as a separate menu item when split navigation is on. Visible only when AudioMuse is enabled on the active server.',
diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts
index 56dadc00..86718e1e 100644
--- a/src/locales/es/settings.ts
+++ b/src/locales/es/settings.ts
@@ -227,6 +227,10 @@ export const settings = {
aboutContributorsLabel: 'Contribuidores',
showChangelogOnUpdate: "Mostrar 'Novedades' al actualizar",
showChangelogOnUpdateDesc: 'Muestra un discreto banner de changelog encima de Now Playing tras una actualización. Clic abre las notas de la versión; X lo oculta.',
+ libraryGridMaxColumnsTitle: 'Rejillas de tarjetas en la biblioteca',
+ libraryGridMaxColumnsPerfHint: 'Más columnas caben más mosaicos por fila y pueden aumentar el trabajo de diseño y pintado; se nota más en bibliotecas muy grandes o equipos lentos.',
+ libraryGridMaxColumnsRangeLabel: 'Máximo de columnas ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Se aplica a vistas de álbum, artista, lista de reproducción, radio, sin conexión y otras con tarjetas. Menos columnas = mosaicos más grandes y suele aligerar la CPU.',
randomMixTitle: 'Lista negra de Mezcla Aleatoria',
luckyMixMenuTitle: 'Mostrar Mezcla Suerte en el menú',
luckyMixMenuDesc: 'Activa Mezcla Suerte en "Crear Mezcla" y como elemento de menú separado cuando la navegación dividida está activa. Solo visible cuando AudioMuse está activo en el servidor actual.',
diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts
index 479766d7..d6384edb 100644
--- a/src/locales/fr/settings.ts
+++ b/src/locales/fr/settings.ts
@@ -225,6 +225,10 @@ export const settings = {
aboutContributorsLabel: 'Contributeurs',
showChangelogOnUpdate: "Afficher 'Quoi de neuf' lors des mises à jour",
showChangelogOnUpdateDesc: "Affiche une bannière discrète du changelog au-dessus de Now Playing après une mise à jour. Un clic ouvre les notes de version, le X la masque.",
+ libraryGridMaxColumnsTitle: 'Grilles de cartes dans la bibliothèque',
+ libraryGridMaxColumnsPerfHint: 'Plus de colonnes signifie plus de tuiles par ligne et davantage de travail de mise en page et de peinture — surtout sur de grosses bibliothèques ou du matériel lent.',
+ libraryGridMaxColumnsRangeLabel: 'Nombre maximal de colonnes ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'S’applique aux vues album, artiste, liste de lecture, radio, hors ligne et autres pages en cartes. Moins de colonnes = tuiles plus grandes et en général moins de charge CPU.',
randomMixTitle: 'Liste noire du mix aléatoire',
luckyMixMenuTitle: 'Afficher Mix Chance dans le menu',
luckyMixMenuDesc: 'Active Mix Chance dans "Créer un mix" et comme entrée séparée quand la navigation est scindée. Visible uniquement si AudioMuse est actif sur le serveur courant.',
diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts
index 1e5015fe..ca7fd64b 100644
--- a/src/locales/nb/settings.ts
+++ b/src/locales/nb/settings.ts
@@ -224,6 +224,10 @@ export const settings = {
aboutContributorsLabel: 'Bidragsytere',
showChangelogOnUpdate: "Vis 'Hva er nytt' ved oppdatering til ny versjon",
showChangelogOnUpdateDesc: "Viser et diskret changelog-banner over Now Playing etter en oppdatering. Klikk åpner versjonsnotatene; X skjuler det.",
+ libraryGridMaxColumnsTitle: 'Kortrutenett i biblioteket',
+ libraryGridMaxColumnsPerfHint: 'Flere kolonner gir flere fliser per rad og kan øke layout- og tegnearbeid — merkbart på veldig store biblioteker eller tregere maskiner.',
+ libraryGridMaxColumnsRangeLabel: 'Maksimalt antall kolonner ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Gjelder album-, artist-, spilleliste-, radio-, offline- og andre kortbaserte bibliotekvisninger. Færre kolonner = større fliser og vanligvis mindre CPU-belastning.',
randomMixTitle: 'Svarteliste for tilfeldig miks',
luckyMixMenuTitle: 'Vis Lykkemiks i menyen',
luckyMixMenuDesc: 'Aktiverer Lykkemiks i "Lag en miks" og som eget menypunkt når delt navigasjon er aktiv. Vises bare når AudioMuse er aktiv på gjeldende server.',
diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts
index 421fb0fd..037d5abf 100644
--- a/src/locales/nl/settings.ts
+++ b/src/locales/nl/settings.ts
@@ -225,6 +225,10 @@ export const settings = {
aboutContributorsLabel: 'Bijdragers',
showChangelogOnUpdate: "'Wat is nieuw' tonen bij update",
showChangelogOnUpdateDesc: 'Toont een discrete changelog-banner boven Now Playing na een update. Klik opent de release-notities; X verbergt hem.',
+ libraryGridMaxColumnsTitle: 'Kaartenrasters in de bibliotheek',
+ libraryGridMaxColumnsPerfHint: 'Meer kolommen betekent meer tegels per rij en vaak meer lay-out- en schilderwerk — merkbaar bij zeer grote bibliotheken of tragere hardware.',
+ libraryGridMaxColumnsRangeLabel: 'Maximum aantal kolommen ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Geldt voor album-, artiest-, afspeellijst-, radio-, offline- en andere kaartweergaven. Minder kolommen = grotere tegels en meestal minder CPU-belasting.',
randomMixTitle: 'Willekeurige mix-blacklist',
luckyMixMenuTitle: 'Toon Geluksmix in menu',
luckyMixMenuDesc: 'Schakelt Geluksmix in bij "Mix samenstellen" en als apart menu-item bij gesplitste navigatie. Alleen zichtbaar wanneer AudioMuse actief is op de huidige server.',
diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts
index 28b9362a..c1535c83 100644
--- a/src/locales/ro/settings.ts
+++ b/src/locales/ro/settings.ts
@@ -231,6 +231,10 @@ export const settings = {
aboutContributorsLabel: 'Contribuabili',
showChangelogOnUpdate: "Arată 'Ce este nou' în update",
showChangelogOnUpdateDesc: "Arată un banner discret cu lista modificărilor deasupra Now Playing după un update. Apasă pentru a deschide lista modificărilor; X îl respinge.",
+ libraryGridMaxColumnsTitle: 'Grile cu carduri în bibliotecă',
+ libraryGridMaxColumnsPerfHint: 'Mai multe coloane înseamnă mai multe plăcuțe pe rând și mai multă muncă de layout și desen — se simte pe biblioteci foarte mari sau hardware lent.',
+ libraryGridMaxColumnsRangeLabel: 'Număr maxim de coloane ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Se aplică la albume, artiști, liste de redare, radio, offline și alte ecrane cu carduri. Mai puține coloane = plăcuțe mai mari și de obicei mai puțină încărcare pe CPU.',
randomMixTitle: 'Lista neagră Mix Aleatoriu',
luckyMixMenuTitle: 'Arată Mixul Norocos în meniu',
luckyMixMenuDesc: 'Activează Mixul Norocos în Construiește un Mix și ca un element separat în meniu când navigarea împărțită este pornită. Vizibil doar când AudioMuse este activat pe serverul activ.',
diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts
index e7920bb6..cf59ae2a 100644
--- a/src/locales/ru/settings.ts
+++ b/src/locales/ru/settings.ts
@@ -237,6 +237,10 @@ export const settings = {
aboutContributorsLabel: 'Участники',
showChangelogOnUpdate: 'Показывать «Что нового» после обновления',
showChangelogOnUpdateDesc: 'После обновления над «Сейчас играет» появится ненавязчивый баннер журнала изменений. Клик открывает заметки о выпуске, X скрывает.',
+ libraryGridMaxColumnsTitle: 'Сетки карточек в библиотеке',
+ libraryGridMaxColumnsPerfHint: 'Больше колонок — больше плиток в ряд и больше работы по вёрстке и отрисовке; на очень больших библиотеках или слабом железе это заметнее.',
+ libraryGridMaxColumnsRangeLabel: 'Максимум колонок ({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: 'Действует для альбомов, исполнителей, плейлистов, радио, офлайн-библиотеки и других экранов с карточками. Меньше колонок — крупнее плитки и обычно меньше нагрузка на CPU.',
randomMixTitle: 'Чёрный список случайного микса',
luckyMixMenuTitle: 'Показывать «Мне повезёт» в меню',
luckyMixMenuDesc:
diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts
index 16c3d77d..d59b189b 100644
--- a/src/locales/zh/settings.ts
+++ b/src/locales/zh/settings.ts
@@ -225,6 +225,10 @@ export const settings = {
aboutContributorsLabel: '贡献者',
showChangelogOnUpdate: '更新时显示"新功能"',
showChangelogOnUpdateDesc: '更新后在「正在播放」上方显示一个低调的更新日志横幅。点击打开发行说明,X 按钮关闭。',
+ libraryGridMaxColumnsTitle: '资料库卡片网格',
+ libraryGridMaxColumnsPerfHint: '列数越多,每行显示的卡片越多,布局与绘制开销通常也越大;在超大曲库或较慢设备上更明显。',
+ libraryGridMaxColumnsRangeLabel: '最大列数({{min}}–{{max}})',
+ libraryGridMaxColumnsDesc: '适用于专辑、艺人、播放列表、电台、离线及其他卡片式资料库视图。列数越少卡片越大,通常对 CPU 更友好。',
randomMixTitle: '随机混音黑名单',
luckyMixMenuTitle: '在菜单中显示“好运混音”',
luckyMixMenuDesc: '在“创建混音”中启用“好运混音”,并在分离导航时作为独立菜单项显示。仅当当前服务器启用 AudioMuse 时可见。',
diff --git a/src/store/authStore.settings.test.ts b/src/store/authStore.settings.test.ts
index b9e790e2..60fd96fe 100644
--- a/src/store/authStore.settings.test.ts
+++ b/src/store/authStore.settings.test.ts
@@ -30,6 +30,10 @@ vi.mock('@/api/subsonic', () => ({
import { useAuthStore } from './authStore';
import { resetAuthStore, resetPlayerStore } from '@/test/helpers/storeReset';
import { onInvoke } from '@/test/mocks/tauri';
+import {
+ LIBRARY_GRID_MAX_COLUMNS_MAX,
+ LIBRARY_GRID_MAX_COLUMNS_MIN,
+} from './authStoreDefaults';
beforeEach(() => {
resetAuthStore();
@@ -120,6 +124,17 @@ describe('setters with validation / clamping', () => {
expect(useAuthStore.getState().trackPreviewDurationSec).toBe(31);
});
+ it('setLibraryGridMaxColumns clamps to the allowed column range', () => {
+ useAuthStore.getState().setLibraryGridMaxColumns(99);
+ expect(useAuthStore.getState().libraryGridMaxColumns).toBe(LIBRARY_GRID_MAX_COLUMNS_MAX);
+
+ useAuthStore.getState().setLibraryGridMaxColumns(1);
+ expect(useAuthStore.getState().libraryGridMaxColumns).toBe(LIBRARY_GRID_MAX_COLUMNS_MIN);
+
+ useAuthStore.getState().setLibraryGridMaxColumns(6);
+ expect(useAuthStore.getState().libraryGridMaxColumns).toBe(6);
+ });
+
it('setTrackPreviewsEnabled coerces truthy/falsy to boolean', () => {
useAuthStore.getState().setTrackPreviewsEnabled('yes' as unknown as boolean);
expect(useAuthStore.getState().trackPreviewsEnabled).toBe(true);
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
index 0b41eda1..fa786245 100644
--- a/src/store/authStore.ts
+++ b/src/store/authStore.ts
@@ -17,6 +17,7 @@ import {
DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
DEFAULT_LYRICS_SOURCES,
DEFAULT_TRACK_PREVIEW_LOCATIONS,
+ DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
} from './authStoreDefaults';
import { computeAuthStoreRehydration } from './authStoreRehydrate';
import type { AuthState } from './authStoreTypes';
@@ -58,6 +59,7 @@ export const useAuthStore = create()(
infiniteQueueEnabled: false,
preservePlayNextOrder: false,
showArtistImages: false,
+ libraryGridMaxColumns: DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
showTrayIcon: true,
minimizeToTray: false,
showOrbitTrigger: true,
diff --git a/src/store/authStoreDefaults.ts b/src/store/authStoreDefaults.ts
index 37bcfa77..0dc9f512 100644
--- a/src/store/authStoreDefaults.ts
+++ b/src/store/authStoreDefaults.ts
@@ -38,3 +38,8 @@ export const DEFAULT_LYRICS_SOURCES: LyricsSourceConfig[] = [
export const MIX_MIN_RATING_FILTER_MAX_STARS = 3;
export const RANDOM_MIX_SIZE_OPTIONS: readonly number[] = [50, 75, 100, 125, 150];
+
+/** Default max columns for album/artist/playlist card grids (Settings → Library). */
+export const DEFAULT_LIBRARY_GRID_MAX_COLUMNS = 6;
+export const LIBRARY_GRID_MAX_COLUMNS_MIN = 4;
+export const LIBRARY_GRID_MAX_COLUMNS_MAX = 12;
diff --git a/src/store/authStoreHelpers.ts b/src/store/authStoreHelpers.ts
index 2105f374..2421e5da 100644
--- a/src/store/authStoreHelpers.ts
+++ b/src/store/authStoreHelpers.ts
@@ -1,6 +1,9 @@
import { clampStoredLoudnessPreAnalysisAttenuationRefDb } from '../utils/audio/loudnessPreAnalysisSlider';
import {
DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
+ DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
+ LIBRARY_GRID_MAX_COLUMNS_MAX,
+ LIBRARY_GRID_MAX_COLUMNS_MIN,
LOUDNESS_LUFS_PRESETS,
MIX_MIN_RATING_FILTER_MAX_STARS,
RANDOM_MIX_SIZE_OPTIONS,
@@ -41,6 +44,13 @@ export function clampRandomMixSize(v: number): number {
return nearest;
}
+/** Persisted max columns for library card grids (albums, artists, playlists, …). */
+export function clampLibraryGridMaxColumns(v: unknown): number {
+ const n = typeof v === 'number' ? v : Number(v);
+ if (!Number.isFinite(n)) return DEFAULT_LIBRARY_GRID_MAX_COLUMNS;
+ return Math.max(LIBRARY_GRID_MAX_COLUMNS_MIN, Math.min(LIBRARY_GRID_MAX_COLUMNS_MAX, Math.round(n)));
+}
+
export function clampSkipStarThreshold(v: number): number {
if (!Number.isFinite(v)) return 3;
return Math.max(1, Math.min(99, Math.round(v)));
diff --git a/src/store/authStoreRehydrate.ts b/src/store/authStoreRehydrate.ts
index 7ecb2b59..969b0d4c 100644
--- a/src/store/authStoreRehydrate.ts
+++ b/src/store/authStoreRehydrate.ts
@@ -7,6 +7,7 @@ import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from './authStoreDefault
import {
clampMixFilterMinStars,
clampRandomMixSize,
+ clampLibraryGridMaxColumns,
sanitizeLoudnessLufsPreset,
sanitizeLoudnessPreAnalysisFromStorage,
sanitizeSkipStarCounts,
@@ -116,6 +117,9 @@ export function computeAuthStoreRehydration(state: AuthState): Partial void;
setPreservePlayNextOrder: (v: boolean) => void;
setShowArtistImages: (v: boolean) => void;
+ setLibraryGridMaxColumns: (v: number) => void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setShowOrbitTrigger: (v: boolean) => void;
diff --git a/src/store/authUiAppearanceActions.ts b/src/store/authUiAppearanceActions.ts
index 477b83f5..bd964c05 100644
--- a/src/store/authUiAppearanceActions.ts
+++ b/src/store/authUiAppearanceActions.ts
@@ -1,4 +1,5 @@
import type { AuthState } from './authStoreTypes';
+import { clampLibraryGridMaxColumns } from './authStoreHelpers';
type SetState = (
partial: Partial | ((state: AuthState) => Partial),
@@ -12,6 +13,7 @@ type SetState = (
export function createUiAppearanceActions(set: SetState): Pick<
AuthState,
| 'setShowArtistImages'
+ | 'setLibraryGridMaxColumns'
| 'setShowTrayIcon'
| 'setMinimizeToTray'
| 'setShowOrbitTrigger'
@@ -30,6 +32,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
> {
return {
setShowArtistImages: (v) => set({ showArtistImages: v }),
+ setLibraryGridMaxColumns: (v) => set({ libraryGridMaxColumns: clampLibraryGridMaxColumns(v) }),
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }),
diff --git a/src/utils/cardGridLayout.test.ts b/src/utils/cardGridLayout.test.ts
index 75faff41..cbdb9e67 100644
--- a/src/utils/cardGridLayout.test.ts
+++ b/src/utils/cardGridLayout.test.ts
@@ -1,16 +1,26 @@
import { describe, expect, it } from 'vitest';
-import { computeCardGridColumnCount, CARD_GRID_MAX_COLS } from './cardGridLayout';
+import { computeCardGridColumnCount } from './cardGridLayout';
+import { LIBRARY_GRID_MAX_COLUMNS_MAX, LIBRARY_GRID_MAX_COLUMNS_MIN } from '../store/authStoreDefaults';
describe('computeCardGridColumnCount', () => {
- it('never exceeds CARD_GRID_MAX_COLS', () => {
- expect(computeCardGridColumnCount(20_000)).toBe(CARD_GRID_MAX_COLS);
+ it('never exceeds the configured max', () => {
+ expect(computeCardGridColumnCount(20_000, 6)).toBe(6);
+ expect(computeCardGridColumnCount(20_000, 4)).toBe(4);
+ });
+
+ it('clamps requested max to store-wide upper bound', () => {
+ expect(computeCardGridColumnCount(20_000, 99)).toBe(LIBRARY_GRID_MAX_COLUMNS_MAX);
+ });
+
+ it('clamps requested max to store-wide lower bound', () => {
+ expect(computeCardGridColumnCount(20_000, 2)).toBe(LIBRARY_GRID_MAX_COLUMNS_MIN);
});
it('returns at least one column', () => {
- expect(computeCardGridColumnCount(50)).toBe(1);
+ expect(computeCardGridColumnCount(50, 6)).toBe(1);
});
- it('uses six columns on wide desktop widths', () => {
- expect(computeCardGridColumnCount(1200)).toBe(6);
+ it('uses six columns on wide desktop when max allows', () => {
+ expect(computeCardGridColumnCount(1200, 6)).toBe(6);
});
});
diff --git a/src/utils/cardGridLayout.ts b/src/utils/cardGridLayout.ts
index 9b10e4d0..dd82a95c 100644
--- a/src/utils/cardGridLayout.ts
+++ b/src/utils/cardGridLayout.ts
@@ -3,15 +3,27 @@
* and row-height estimates derived from measured cell width (TanStack virtual rows).
*/
+import {
+ DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
+ LIBRARY_GRID_MAX_COLUMNS_MAX,
+ LIBRARY_GRID_MAX_COLUMNS_MIN,
+} from '../store/authStoreDefaults';
+
export const CARD_GRID_GAP_PX = 16;
export const CARD_GRID_MIN_TILE_PX = 140;
-export const CARD_GRID_MAX_COLS = 6;
-export function computeCardGridColumnCount(containerWidthPx: number): number {
+/** @deprecated use `DEFAULT_LIBRARY_GRID_MAX_COLUMNS` from `authStoreDefaults` */
+export const CARD_GRID_MAX_COLS = DEFAULT_LIBRARY_GRID_MAX_COLUMNS;
+
+export function computeCardGridColumnCount(containerWidthPx: number, maxColumns: number): number {
+ const cap = Math.max(
+ LIBRARY_GRID_MAX_COLUMNS_MIN,
+ Math.min(LIBRARY_GRID_MAX_COLUMNS_MAX, Math.round(maxColumns)),
+ );
const raw = Math.floor(
(containerWidthPx + CARD_GRID_GAP_PX) / (CARD_GRID_MIN_TILE_PX + CARD_GRID_GAP_PX),
);
- return Math.min(CARD_GRID_MAX_COLS, Math.max(1, raw));
+ return Math.min(cap, Math.max(1, raw));
}
export function computeCellWidthPx(containerWidthPx: number, columnCount: number): number {