From c0a13b5f87e059020f111de438bbb8b870f2256a Mon Sep 17 00:00:00 2001 From: CRT Date: Fri, 23 Oct 2026 09:49:47 +0800 Subject: [PATCH] Desktop: wide tables keep their first column in view while scrolling; compact table density (Settings > Appearance, Ctrl+K) --- apps/desktop/src/App.tsx | 3 ++- apps/desktop/src/design.css | 6 ++++++ apps/desktop/src/i18n/en.ts | 4 ++++ apps/desktop/src/i18n/tr.ts | 4 ++++ apps/desktop/src/prefs.ts | 6 +++++- apps/desktop/src/screens/Settings.tsx | 2 ++ 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index c90e4ee..94ee3c7 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -160,10 +160,11 @@ export function App({ onTheme }: { onTheme: () => void }) { c.push({ id: 'act-check', group: G.act, title: t('action.checkConfigs'), icon: 'shield', run: () => { d.refreshPortfolio(false); } }); c.push({ id: 'act-add', group: G.act, title: t('action.addFolder'), icon: 'folderPlus', run: () => go({ screen: 'apps' }) }); c.push({ id: 'act-account', group: G.act, title: t('action.addAccount'), icon: 'key', run: () => go({ screen: 'accounts' }) }); + c.push({ id: 'act-density', group: G.act, title: t('action.toggleDensity'), icon: 'grid', keywords: 'dense compact table density rows', run: () => update({ ...prefs, density: prefs.density === 'compact' ? 'comfortable' : 'compact' }) }); c.push({ id: 'act-theme', group: G.act, title: t('action.toggleTheme'), icon: 'moon', keywords: 'dark light', run: onTheme }); c.push({ id: 'act-welcome', group: G.act, title: t('action.welcome'), icon: 'sparkle', keywords: 'onboarding start guide', run: () => setWelcome(true) }); return c; - }, [d, visible, app, tabs, route, go, onTheme, exports, runExport, showIgnored, stepApp]); + }, [d, visible, app, tabs, route, go, onTheme, exports, runExport, showIgnored, stepApp, prefs, update]); const crumbs: { label: string; to?: Route }[] = route.screen === 'portfolio' ? [{ label: t('nav.portfolio') }] : route.screen === 'app' ? [{ label: t('nav.portfolio'), to: { screen: 'portfolio' } }, { label: app?.name ?? '', to: app ? { screen: 'app', appId: app.key, tab: 'releases' } : undefined }, { label: t(TAB_KEY[route.tab]) }] diff --git a/apps/desktop/src/design.css b/apps/desktop/src/design.css index 6db53a4..fc782ca 100644 --- a/apps/desktop/src/design.css +++ b/apps/desktop/src/design.css @@ -239,3 +239,9 @@ @media (max-width: 1560px) { .ds-tabs .ds-tab { padding: 0 10px; gap: 6px; } .ds-tabs .ds-tab svg { width: 15px; height: 15px; } } .dr-cmd { display: block; margin: 10px 0 8px; padding: 6px 10px; border-radius: var(--ds-r-xs); background: var(--ds-inset); overflow-wrap: anywhere; } .dr-out { max-height: 320px; overflow: auto; margin: 0; padding: 10px 12px; border-radius: var(--ds-r-sm); background: var(--ds-inset); border: 1px solid var(--ds-stroke); font: 12px/1.5 var(--mono, ui-monospace, monospace); white-space: pre-wrap; } +/* wide tables: the first column (the row's name) stays in view while the rest scrolls sideways */ +.table-wrap .table tr > :first-child, .ds-table-wrap .table tr > :first-child { position: sticky; left: 0; z-index: 1; background: var(--ds-surface, var(--surface)); } +.table-wrap .table thead tr > :first-child, .ds-table-wrap .table thead tr > :first-child { z-index: 3; } +.table-wrap .table tbody tr:hover > :first-child, .ds-table-wrap .table tbody tr:hover > :first-child { background: color-mix(in srgb, var(--tint, var(--accent-solid)) 6%, var(--ds-surface, var(--surface))); } +/* Settings > Appearance > Tables: compact */ +:root[data-density="compact"] .table th, :root[data-density="compact"] .table td { padding-top: 5px; padding-bottom: 5px; padding-left: 8px; padding-right: 8px; font-size: 12.5px; } diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index b96579e..127bf41 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -85,6 +85,10 @@ export const en = { 'preview.lead': "Dry run: nothing is changed yet. Continue opens the confirmation, which runs the dry run once more and then applies it.", 'preview.more': "… {count} more lines", 'preview.nothing': "The dry run reports no change.", + 'settings.density': "Tables", + 'settings.comfortable': "Comfortable", + 'settings.compact': "Compact", + 'action.toggleDensity': "Compact / comfortable tables", 'palette.gActions': 'Actions', 'palette.openApp': 'Open', diff --git a/apps/desktop/src/i18n/tr.ts b/apps/desktop/src/i18n/tr.ts index 977c9b4..f88fe73 100644 --- a/apps/desktop/src/i18n/tr.ts +++ b/apps/desktop/src/i18n/tr.ts @@ -87,6 +87,10 @@ export const tr: Record = { 'preview.lead': "Deneme çalıştırması: henüz hiçbir şey değişmedi. Devam onay penceresini açar; deneme bir kez daha çalışır, sonra uygulanır.", 'preview.more': "… {count} satır daha", 'preview.nothing': "Deneme çalıştırması bir değişiklik bildirmiyor.", + 'settings.density': "Tablolar", + 'settings.comfortable': "Rahat", + 'settings.compact': "Sıkı", + 'action.toggleDensity': "Sıkı / rahat tablolar", 'palette.gActions': 'İşlemler', 'palette.openApp': 'Aç', diff --git a/apps/desktop/src/prefs.ts b/apps/desktop/src/prefs.ts index 7c69eae..7919bbb 100644 --- a/apps/desktop/src/prefs.ts +++ b/apps/desktop/src/prefs.ts @@ -3,7 +3,9 @@ export type Theme = 'system' | 'light' | 'dark'; export type Motion = 'system' | 'reduce'; export type Lang = 'system' | 'en' | 'tr'; -export interface Prefs { theme: Theme; motion: Motion; lang: Lang; onboarded: boolean } +/** Tables: comfortable rows, or compact (smaller padding) for wide data on small windows. */ +export type Density = 'comfortable' | 'compact'; +export interface Prefs { theme: Theme; motion: Motion; lang: Lang; onboarded: boolean; density?: Density } export const DEFAULT_PREFS: Prefs = { theme: 'system', motion: 'system', lang: 'system', onboarded: false }; const KEY = 'storebound.prefs.v1'; @@ -16,6 +18,7 @@ export function parsePrefs(raw: string | null): Prefs { motion: v.motion === 'reduce' ? 'reduce' : 'system', lang: v.lang === 'en' || v.lang === 'tr' ? v.lang : 'system', onboarded: v.onboarded === true, + ...(v.density === 'compact' ? { density: 'compact' as const } : {}), }; } catch { return { ...DEFAULT_PREFS }; } } @@ -32,6 +35,7 @@ export function savePrefs(p: Prefs): void { export function applyPrefs(p: Prefs, root: { dataset: DOMStringMap; lang?: string } = document.documentElement, systemLang = 'en'): void { if (p.theme === 'system') delete root.dataset.theme; else root.dataset.theme = p.theme; if (p.motion === 'reduce') root.dataset.motion = 'reduce'; else delete root.dataset.motion; + if (p.density === 'compact') root.dataset.density = 'compact'; else delete root.dataset.density; root.lang = resolveLang(p.lang, systemLang); } diff --git a/apps/desktop/src/screens/Settings.tsx b/apps/desktop/src/screens/Settings.tsx index 0c8a69e..cde32f6 100644 --- a/apps/desktop/src/screens/Settings.tsx +++ b/apps/desktop/src/screens/Settings.tsx @@ -182,6 +182,8 @@ export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void; update({ ...prefs, theme })} label={t('settings.theme')} options={[['system', t('settings.system'), 'settings'], ['light', t('settings.light'), 'sun'], ['dark', t('settings.dark'), 'moon']]} />
{t('settings.motion')} update({ ...prefs, motion })} label={t('settings.motion')} options={[['system', t('settings.followSystem')], ['reduce', t('settings.reduce')]]} />
+
{t('settings.density')} + update({ ...prefs, density })} label={t('settings.density')} options={[['comfortable', t('settings.comfortable'), 'layers'], ['compact', t('settings.compact'), 'grid']]} />