From 9dd40ce1dcfb520d478b09158abb4c4bcf333de0 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Fri, 27 Feb 2026 23:49:24 -0600 Subject: [PATCH] Horizontal scroll strip for theme grid on mobile onboarding --- src/components/InitialSyncGate.tsx | 2 +- src/components/ThemeSelector.tsx | 201 +++++++++++++++-------------- 2 files changed, 105 insertions(+), 98 deletions(-) diff --git a/src/components/InitialSyncGate.tsx b/src/components/InitialSyncGate.tsx index b0402e5d..b39c18a7 100644 --- a/src/components/InitialSyncGate.tsx +++ b/src/components/InitialSyncGate.tsx @@ -968,7 +968,7 @@ function ThemeStep({

Pick a theme that feels right.

- + {isFirst ? ( + ); +} + /** * Renders the grid of theme options (builtins + presets + user themes). * Applies selection immediately via `useTheme`. Calls `onSelect` after @@ -135,8 +172,14 @@ export function ThemeGrid({ editingTheme?: ThemeDefinition | null; /** Callback to update editing theme state in the parent. */ onEditingThemeChange?: (def: ThemeDefinition | null) => void; - /** Grid column count: 'responsive' = 2 cols on mobile / 3 on sidebar breakpoint, 'sm' = 2 cols on mobile / 3 on sm+, '2' = always 2 cols */ - columns?: 'responsive' | 'sm' | '2'; + /** + * Layout mode: + * - 'responsive': 2-col grid on mobile, 3-col at sidebar breakpoint (900px) + * - 'sm': 2-col grid on mobile, 3-col at sm (640px) + * - '2': always 2-col grid + * - 'scroll': horizontal scrolling strip on mobile, 3-col grid at sm+ + */ + columns?: 'responsive' | 'sm' | '2' | 'scroll'; }) { const { theme, customTheme, themes, setTheme, applyCustomTheme } = useTheme(); const { user } = useCurrentUser(); @@ -183,103 +226,67 @@ export function ThemeGrid({ onSelect?.(); }, [applyCustomTheme, onSelect, onEditingThemeChange]); - const gridClass = - columns === '2' ? 'grid grid-cols-2 gap-3' : - columns === 'sm' ? 'grid grid-cols-2 sm:grid-cols-3 gap-3' : - 'grid grid-cols-2 sidebar:grid-cols-3 gap-3'; + const isScroll = columns === 'scroll'; + + const wrapperClass = isScroll + ? 'flex sm:grid sm:grid-cols-3 gap-3 overflow-x-auto pb-1 -mx-1 px-1 snap-x snap-mandatory' + : columns === '2' ? 'grid grid-cols-2 gap-3' + : columns === 'sm' ? 'grid grid-cols-2 sm:grid-cols-3 gap-3' + : 'grid grid-cols-2 sidebar:grid-cols-3 gap-3'; + + const renderBuiltins = () => builtinOptions.map((option) => { + if (option.id === 'system') { + const isActive = theme === 'system'; + const lightTokens = coreToTokens(resolveThemeConfig('light', themes).colors); + const darkTokens = coreToTokens(resolveThemeConfig('dark', themes).colors); + return ( + handleSelectBuiltin('system')}> +
+ + + {isActive && ( +
+ +
+ )} +
+
+ ); + } + const colors = resolveThemeConfig(option.id as 'light' | 'dark', themes).colors; + const isActive = theme === option.id; + return ( + handleSelectBuiltin(option.id)}> + + + ); + }); + + const renderPresets = () => presetOptions.map((preset) => { + const isActive = isPresetActive(preset.colors); + return ( + handleSelectPreset(preset)}> + + + ); + }); + + const renderUserThemes = () => userThemes.data?.map((def) => { + const isActive = isUserThemeActive(def); + return ( + handleSelectUserTheme(def)}> + + + ); + }); return ( -
- {builtinOptions.map((option) => { - if (option.id === 'system') { - const isActive = theme === 'system'; - const lightTokens = coreToTokens(resolveThemeConfig('light', themes).colors); - const darkTokens = coreToTokens(resolveThemeConfig('dark', themes).colors); - - return ( - - ); - } - - const colors = resolveThemeConfig(option.id as 'light' | 'dark', themes).colors; - const isActive = theme === option.id; - - return ( - - ); - })} - - {presetOptions.map((preset) => { - const isActive = isPresetActive(preset.colors); - return ( - - ); - })} - - {userThemes.data?.map((def) => { - const isActive = isUserThemeActive(def); - return ( - - ); - })} +
+ {renderBuiltins()} + {renderPresets()} + {renderUserThemes()}
); }