@@ -5,6 +5,15 @@ import { SectionCard } from '../components/SectionCard.tsx'
55import { RepoSnapshot } from '../features/repos/RepoSnapshot.tsx'
66import { SettingsPanel } from '../features/settings/SettingsPanel.tsx'
77import { StatusStrip } from '../features/status/StatusStrip.tsx'
8+ import { ThemeControls } from '../features/theme/ThemeControls.tsx'
9+ import {
10+ applyThemePreference ,
11+ persistThemePreference ,
12+ themePresets ,
13+ type ThemeMode ,
14+ type ThemePreference ,
15+ type ThemePreset ,
16+ } from '../features/theme/theme.ts'
817import {
918 fetchWorkspaceSummary ,
1019 generateRepoCover ,
@@ -22,6 +31,9 @@ import type { RepoType, WorkspaceRepo, WorkspaceSummary } from '../types/workspa
2231import './app.css'
2332
2433type RepoFilterValue = RepoType | 'all' | 'external' | 'runnable'
34+ type AppProps = {
35+ initialThemePreference : ThemePreference
36+ }
2537
2638function formatGeneratedAt ( value : string ) {
2739 return new Intl . DateTimeFormat ( undefined , {
@@ -110,7 +122,7 @@ function filterRepos(
110122 } )
111123}
112124
113- export function App ( ) {
125+ export function App ( { initialThemePreference } : AppProps ) {
114126 const [ summary , setSummary ] = useState < WorkspaceSummary | null > ( null )
115127 const [ loading , setLoading ] = useState ( true )
116128 const [ error , setError ] = useState < string | null > ( null )
@@ -119,6 +131,7 @@ export function App() {
119131 const [ searchTerm , setSearchTerm ] = useState ( '' )
120132 const [ selectedPath , setSelectedPath ] = useState < string | null > ( null )
121133 const [ selectedFilter , setSelectedFilter ] = useState < RepoFilterValue > ( 'all' )
134+ const [ themePreference , setThemePreference ] = useState ( initialThemePreference )
122135 const deferredSearchTerm = useDeferredValue ( searchTerm )
123136 const lastRecordedSelectionRef = useRef < string | null > ( null )
124137
@@ -357,13 +370,20 @@ export function App() {
357370 }
358371 } , [ ] )
359372
373+ useEffect ( ( ) => {
374+ applyThemePreference ( themePreference )
375+ persistThemePreference ( themePreference )
376+ } , [ themePreference ] )
377+
360378 const availableTypes = summary
361379 ? [ ...new Set ( summary . repos . map ( ( repo ) => repo . type ) ) ] . sort ( )
362380 : [ ]
363381 const normalizedSearch = deferredSearchTerm . trim ( ) . toLowerCase ( )
364382 const filteredRepos = summary
365383 ? filterRepos ( summary . repos , normalizedSearch , selectedFilter )
366384 : [ ]
385+ const activeThemePreset =
386+ themePresets . find ( ( preset ) => preset . id === themePreference . preset ) ?? themePresets [ 0 ]
367387
368388 useEffect ( ( ) => {
369389 const nextFilteredRepos = summary
@@ -419,38 +439,64 @@ export function App() {
419439
420440 const generatedAt = summary ? formatGeneratedAt ( summary . generatedAt ) : 'Waiting for API'
421441
442+ function handleThemePresetChange ( preset : ThemePreset ) {
443+ setThemePreference ( ( currentPreference ) => ( {
444+ ...currentPreference ,
445+ preset,
446+ } ) )
447+ }
448+
449+ function handleThemeModeChange ( mode : ThemeMode ) {
450+ setThemePreference ( ( currentPreference ) => ( {
451+ ...currentPreference ,
452+ mode,
453+ } ) )
454+ }
455+
422456 return (
423457 < div className = "app-shell" >
424458 < header className = "hero-panel reveal" >
425- < div className = "hero-copy" >
426- < p className = "eyebrow" > Codex Workspace Control Centre</ p >
427- < h1 > Workspace Hub</ h1 >
428- < p className = "hero-text" >
429- A local dashboard for discovering repositories, tracking runtime
430- state, and keeping direct previews as the default path.
431- </ p >
432- </ div >
433-
434- < div className = "hero-actions" >
435- < button
436- className = "primary-button"
437- disabled = { loading }
438- onClick = { ( ) => {
439- void loadSummary ( )
440- } }
441- type = "button"
442- >
443- { loading ? 'Refreshing snapshot...' : 'Refresh snapshot' }
444- </ button >
445-
446- < a className = "secondary-link" href = "/api/health" rel = "noreferrer" target = "_blank" >
447- API health
448- </ a >
449- </ div >
459+ < div className = "hero-grid" >
460+ < div className = "hero-main" >
461+ < div className = "hero-copy" >
462+ < p className = "eyebrow" > Codex Workspace Control Centre</ p >
463+ < h1 > Workspace Hub</ h1 >
464+ < p className = "hero-text" >
465+ A local dashboard for discovering repositories, tracking runtime
466+ state, and keeping direct previews as the default path.
467+ </ p >
468+ < p className = "hero-subtext" > { activeThemePreset . description } </ p >
469+ </ div >
470+
471+ < div className = "hero-actions" >
472+ < button
473+ className = "primary-button"
474+ disabled = { loading }
475+ onClick = { ( ) => {
476+ void loadSummary ( )
477+ } }
478+ type = "button"
479+ >
480+ { loading ? 'Refreshing snapshot...' : 'Refresh snapshot' }
481+ </ button >
482+
483+ < a className = "secondary-link" href = "/api/health" rel = "noreferrer" target = "_blank" >
484+ API health
485+ </ a >
486+ </ div >
487+
488+ < div className = "hero-meta" >
489+ < span > Workspace root: { summary ?. workspaceRoot ?? 'Loading...' } </ span >
490+ < span > Last sync: { generatedAt } </ span >
491+ </ div >
492+ </ div >
450493
451- < div className = "hero-meta" >
452- < span > Workspace root: { summary ?. workspaceRoot ?? 'Loading...' } </ span >
453- < span > Last sync: { generatedAt } </ span >
494+ < ThemeControls
495+ mode = { themePreference . mode }
496+ onModeChange = { handleThemeModeChange }
497+ onPresetChange = { handleThemePresetChange }
498+ preset = { themePreference . preset }
499+ />
454500 </ div >
455501 </ header >
456502
0 commit comments