From eb8353125f822142d5cb598e515e7559ad62965e Mon Sep 17 00:00:00 2001 From: Vladyslav Hrabovyii Date: Tue, 7 Jul 2026 21:25:23 +0100 Subject: [PATCH] fix(review): show System in theme menu via shared mode list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review Options menu offered only Light/Dark while the plan, annotate, and Settings surfaces offered System too. Four surfaces each hand-listed the theme modes independently, so the review picker had silently dropped `system` and could never highlight it (its `activeTheme` memo collapsed system to the resolved light/dark). Introduce `THEME_MODES` as one shared list of the mode set with each mode's label and icon; derive `Mode` from it so a mode cannot exist without a render entry, and no surface can omit one. All four pickers (plan, review, Settings, landing toggle) now map the same list; active state is the selected mode everywhere. 🩸 Co-Authored-By: Dexter - @gwynnnplaine's Pi (https://pi.dev/) assistant --- .../components/ReviewHeaderMenu.tsx | 33 ++++-------- packages/ui/components/ModeToggle.tsx | 13 ++--- packages/ui/components/PlanHeaderMenu.tsx | 14 ++--- packages/ui/components/ThemeProvider.tsx | 3 +- packages/ui/components/ThemeTab.tsx | 53 +++++++------------ packages/ui/components/icons/themeIcons.tsx | 2 +- packages/ui/components/themeModes.ts | 23 ++++++++ 7 files changed, 66 insertions(+), 75 deletions(-) create mode 100644 packages/ui/components/themeModes.ts 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'];