fix: make density setting functional across entire UI

Rename "List Density" to "Density" and apply spacing changes globally
instead of only to email list item heights.

- Replace hardcoded padding/gap values with CSS custom properties
  (--density-item-py, --density-item-gap, --density-header-py,
  --density-card-p, --density-sidebar-py) set via JS on :root
- Apply density-responsive spacing to email list items, thread views,
  email viewer, sidebar, navigation rail, contacts, and calendar
- Use inline styles instead of Tailwind arbitrary value classes to
  avoid Turbopack compilation hangs
- Rename listDensity → density in store, types, and components
- Add persist migration (v1 → v2) and onRehydrateStorage callback
- Update all 8 locale files with broadened labels/descriptions
This commit is contained in:
Linus Rath
2026-03-14 18:35:43 +01:00
parent 86673a7b45
commit 838de8e5da
23 changed files with 133 additions and 70 deletions
+62 -17
View File
@@ -19,7 +19,9 @@ let isLoadingFromServer = false;
const SYNC_DEBOUNCE_MS = 2000;
export type FontSize = 'small' | 'medium' | 'large';
export type ListDensity = 'compact' | 'regular' | 'comfortable';
export type Density = 'compact' | 'regular' | 'comfortable';
/** @deprecated Use Density instead */
export type ListDensity = Density;
export type DeleteAction = 'trash' | 'permanent';
export type ReplyMode = 'reply' | 'replyAll';
export type DateFormat = 'regional' | 'iso' | 'custom';
@@ -64,7 +66,7 @@ export const DEFAULT_KEYWORDS: KeywordDefinition[] = [
interface SettingsState {
// Appearance
fontSize: FontSize;
listDensity: ListDensity;
density: Density;
animationsEnabled: boolean;
// Language & Region
@@ -141,7 +143,7 @@ interface SettingsState {
const DEFAULT_SETTINGS = {
// Appearance
fontSize: 'medium' as FontSize,
listDensity: 'regular' as ListDensity,
density: 'regular' as Density,
animationsEnabled: true,
// Language & Region
@@ -199,9 +201,9 @@ export const useSettingsStore = create<SettingsState>()(
applyFontSize(value as FontSize);
}
// Apply list density to document root
if (key === 'listDensity') {
applyListDensity(value as ListDensity);
// Apply density to document root
if (key === 'density') {
applyDensity(value as Density);
}
// Apply animations to document root
@@ -213,7 +215,7 @@ export const useSettingsStore = create<SettingsState>()(
resetToDefaults: () => {
set(DEFAULT_SETTINGS);
applyFontSize(DEFAULT_SETTINGS.fontSize);
applyListDensity(DEFAULT_SETTINGS.listDensity);
applyDensity(DEFAULT_SETTINGS.density);
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
},
@@ -221,7 +223,7 @@ export const useSettingsStore = create<SettingsState>()(
const state = get();
const settings = {
fontSize: state.fontSize,
listDensity: state.listDensity,
density: state.density,
animationsEnabled: state.animationsEnabled,
dateFormat: state.dateFormat,
timeFormat: state.timeFormat,
@@ -269,7 +271,7 @@ export const useSettingsStore = create<SettingsState>()(
// Apply visual settings
applyFontSize(get().fontSize);
applyListDensity(get().listDensity);
applyDensity(get().density);
applyAnimations(get().animationsEnabled);
// Apply cross-store settings
@@ -395,7 +397,24 @@ export const useSettingsStore = create<SettingsState>()(
}),
{
name: 'settings-storage',
version: 1,
version: 2,
migrate: (persisted, version) => {
const state = persisted as Record<string, unknown>;
if (version < 2 && state.listDensity) {
state.density = state.listDensity;
delete state.listDensity;
}
return state as unknown as SettingsState;
},
onRehydrateStorage: () => {
return (state) => {
if (state) {
applyFontSize(state.fontSize);
applyDensity(state.density);
applyAnimations(state.animationsEnabled);
}
};
},
}
)
);
@@ -413,16 +432,42 @@ function applyFontSize(size: FontSize) {
root.style.setProperty('--font-size-base', sizeMap[size]);
}
function applyListDensity(density: ListDensity) {
function applyDensity(density: Density) {
if (typeof document === 'undefined') return;
const root = document.documentElement;
const densityMap = {
compact: '32px',
regular: '48px',
comfortable: '64px',
const densityValues = {
compact: {
'--list-item-height': 'auto',
'--density-item-py': '4px',
'--density-item-gap': '8px',
'--density-header-py': '6px',
'--density-card-p': '10px',
'--density-sidebar-py': '1px',
},
regular: {
'--list-item-height': '48px',
'--density-item-py': '12px',
'--density-item-gap': '12px',
'--density-header-py': '12px',
'--density-card-p': '16px',
'--density-sidebar-py': '4px',
},
comfortable: {
'--list-item-height': '64px',
'--density-item-py': '16px',
'--density-item-gap': '16px',
'--density-header-py': '16px',
'--density-card-p': '20px',
'--density-sidebar-py': '6px',
},
};
root.style.setProperty('--list-item-height', densityMap[density]);
const values = densityValues[density];
for (const [prop, val] of Object.entries(values)) {
root.style.setProperty(prop, val);
}
}
function applyAnimations(enabled: boolean) {
@@ -440,7 +485,7 @@ function applyAnimations(enabled: boolean) {
if (typeof window !== 'undefined') {
const store = useSettingsStore.getState();
applyFontSize(store.fontSize);
applyListDensity(store.listDensity);
applyDensity(store.density);
applyAnimations(store.animationsEnabled);
// Shared sync function used by all store subscribers