diff --git a/packages/review-editor/components/ReviewHeaderMenu.tsx b/packages/review-editor/components/ReviewHeaderMenu.tsx index ffdaf002b..4004b04d1 100644 --- a/packages/review-editor/components/ReviewHeaderMenu.tsx +++ b/packages/review-editor/components/ReviewHeaderMenu.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { ActionMenu, ActionMenuDivider, @@ -6,6 +6,7 @@ import { ActionMenuSectionLabel, } from '@plannotator/ui/components/ActionMenu'; import { useTheme } from '@plannotator/ui/components/ThemeProvider'; +import { THEME_MODES } from '@plannotator/ui/components/themeModes'; import { MenuVersionSection } from '@plannotator/ui/components/MenuVersionSection'; import { ReviewAgentsIcon } from '@plannotator/ui/components/ReviewAgentsIcon'; import { TextShimmer } from '@plannotator/ui/components/TextShimmer'; @@ -44,10 +45,7 @@ export const ReviewHeaderMenu: React.FC = ({ origin, isWSL = false, }) => { - const { theme, resolvedMode, setTheme } = useTheme(); - const activeTheme = useMemo<'light' | 'dark'>(() => { - return theme === 'system' ? resolvedMode : theme; - }, [resolvedMode, theme]); + const { theme, setTheme } = useTheme(); const showUpdateDot = !!updateInfo?.updateAvailable && !updateInfo.dismissed; @@ -87,21 +85,21 @@ export const ReviewHeaderMenu: React.FC = ({
Theme
- {(['light', 'dark'] as const).map((mode) => ( + {THEME_MODES.map(({ id, label, Icon }) => ( ))}
@@ -213,19 +211,6 @@ const ExportIcon = () => ( ); -const SunIcon = () => ( - - - - -); - -const MoonIcon = () => ( - - - -); - const FileTreeMenuIcon = () => ( diff --git a/packages/ui/components/ModeToggle.tsx b/packages/ui/components/ModeToggle.tsx index 8119d4fc3..1f0a7ecee 100644 --- a/packages/ui/components/ModeToggle.tsx +++ b/packages/ui/components/ModeToggle.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { useTheme } from './ThemeProvider'; +import { THEME_MODES } from './themeModes'; export function ModeToggle() { const { theme, setTheme } = useTheme(); @@ -48,17 +49,17 @@ export function ModeToggle() { {isOpen && (
- {(['light', 'dark', 'system'] as const).map((t) => ( + {THEME_MODES.map(({ id, label }) => ( ))}
diff --git a/packages/ui/components/PlanHeaderMenu.tsx b/packages/ui/components/PlanHeaderMenu.tsx index 2855ecfde..875d26077 100644 --- a/packages/ui/components/PlanHeaderMenu.tsx +++ b/packages/ui/components/PlanHeaderMenu.tsx @@ -6,7 +6,7 @@ import { ActionMenuSectionLabel, } from './ActionMenu'; import { useTheme } from './ThemeProvider'; -import { SunIcon, MoonIcon, SystemIcon } from './icons/themeIcons'; +import { THEME_MODES } from './themeModes'; import { ReviewAgentsIcon } from './ReviewAgentsIcon'; import { MenuVersionSection } from './MenuVersionSection'; import { TextShimmer } from './TextShimmer'; @@ -101,21 +101,21 @@ export const PlanHeaderMenu: React.FC = ({
Theme
- {(['light', 'dark', 'system'] as const).map((mode) => ( + {THEME_MODES.map(({ id, label, Icon }) => ( ))}
diff --git a/packages/ui/components/ThemeProvider.tsx b/packages/ui/components/ThemeProvider.tsx index 6fc8c096d..ee326c37b 100644 --- a/packages/ui/components/ThemeProvider.tsx +++ b/packages/ui/components/ThemeProvider.tsx @@ -1,8 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { storage } from '../utils/storage'; import { BUILT_IN_THEMES, type ThemeInfo } from '../utils/themeRegistry'; - -export type Mode = 'dark' | 'light' | 'system'; +import type { Mode } from './themeModes'; type ThemeProviderState = { // Mode (dark/light/system) — backward-compatible with old "theme" API diff --git a/packages/ui/components/ThemeTab.tsx b/packages/ui/components/ThemeTab.tsx index b3d9cd72f..49c07a7f9 100644 --- a/packages/ui/components/ThemeTab.tsx +++ b/packages/ui/components/ThemeTab.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { useTheme, type Mode } from './ThemeProvider'; -import { SunIcon, MoonIcon, SystemIcon } from './icons/themeIcons'; +import { useTheme } from './ThemeProvider'; +import { THEME_MODES } from './themeModes'; interface ThemeTabProps { onPreview?: () => void; @@ -16,39 +16,22 @@ export const ThemeTab: React.FC = ({ onPreview, compact }) => {
{!compact && }
- {(['dark', 'light', 'system'] as Mode[]).map(m => { - const isActive = mode === m; - return ( - - ); - })} + {THEME_MODES.map(({ id, label, Icon }) => ( + + ))}
{compact && ( diff --git a/packages/ui/components/icons/themeIcons.tsx b/packages/ui/components/icons/themeIcons.tsx index dad95f89c..218a9d4d1 100644 --- a/packages/ui/components/icons/themeIcons.tsx +++ b/packages/ui/components/icons/themeIcons.tsx @@ -12,7 +12,7 @@ import React from 'react'; * the dropdown segmented controls use w-3.5, the settings tab uses w-3. */ -interface IconProps { +export interface IconProps { className?: string; } diff --git a/packages/ui/components/themeModes.ts b/packages/ui/components/themeModes.ts new file mode 100644 index 000000000..4162fe155 --- /dev/null +++ b/packages/ui/components/themeModes.ts @@ -0,0 +1,23 @@ +import type { FC } from 'react'; +import { SunIcon, MoonIcon, SystemIcon } from './icons/themeIcons'; +import type { IconProps } from './icons/themeIcons'; + +/** A theme mode paired with how a picker draws it. */ +interface ThemeMode { + readonly id: string; + readonly label: string; + readonly Icon: FC; +} + +/** The one shared list of theme modes every picker renders. + * `as const` preserves each literal `id`; `satisfies` checks each + * entry's shape without widening it. */ +export const THEME_MODES = [ + { id: 'light', label: 'Light', Icon: SunIcon }, + { id: 'dark', label: 'Dark', Icon: MoonIcon }, + { id: 'system', label: 'System', Icon: SystemIcon }, +] as const satisfies readonly ThemeMode[]; + +/** A theme mode's id — the union of every id in THEME_MODES. + * A mode cannot exist without a render entry. */ +export type Mode = (typeof THEME_MODES)[number]['id'];