Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 18 additions & 23 deletions packages/review-editor/components/ReviewHeaderMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { useMemo } from 'react';
import React from 'react';
import {
ActionMenu,
ActionMenuDivider,
ActionMenuItem,
ActionMenuSectionLabel,
} from '@plannotator/ui/components/ActionMenu';
import { useTheme } from '@plannotator/ui/components/ThemeProvider';
import { SunIcon, MoonIcon, SystemIcon } from '@plannotator/ui/components/icons/themeIcons';
import { getUnsupportedMode } from '@plannotator/ui/utils/themeRegistry';
import { MenuVersionSection } from '@plannotator/ui/components/MenuVersionSection';
import { TextShimmer } from '@plannotator/ui/components/TextShimmer';
import { modKey } from '@plannotator/ui/utils/platform';
Expand Down Expand Up @@ -37,10 +39,12 @@ export const ReviewHeaderMenu: React.FC<ReviewHeaderMenuProps> = ({
origin,
isWSL = false,
}) => {
const { theme, resolvedMode, setTheme } = useTheme();
const activeTheme = useMemo<'light' | 'dark'>(() => {
return theme === 'system' ? resolvedMode : theme;
}, [resolvedMode, theme]);
const { theme, setTheme, colorTheme } = useTheme();
const unsupportedMode = getUnsupportedMode(colorTheme);
// When the palette can't render the stored mode, the UI is forced to the
// opposite — highlight the mode actually shown instead of the dead one.
const highlightedTheme =
theme === unsupportedMode ? (unsupportedMode === 'light' ? 'dark' : 'light') : theme;

const showUpdateDot = !!updateInfo?.updateAvailable && !updateInfo.dismissed;

Expand Down Expand Up @@ -80,20 +84,24 @@ export const ReviewHeaderMenu: React.FC<ReviewHeaderMenuProps> = ({
<div className="px-3 py-2 space-y-1.5">
<ActionMenuSectionLabel>Theme</ActionMenuSectionLabel>
<div className="flex items-center gap-1 rounded-lg bg-muted/50 p-0.5">
{(['light', 'dark'] as const).map((mode) => (
{(['light', 'dark', 'system'] as const).map((mode) => (
<button
key={mode}
disabled={mode === unsupportedMode}
title={mode === unsupportedMode ? 'Not supported by the current color theme' : undefined}
onClick={() => {
closeMenu();
setTheme(mode);
}}
className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium transition-colors ${
activeTheme === mode
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
mode === unsupportedMode
? 'text-muted-foreground opacity-40 cursor-not-allowed'
: highlightedTheme === mode
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{mode === 'light' ? <SunIcon /> : <MoonIcon />}
{mode === 'light' ? <SunIcon /> : mode === 'dark' ? <MoonIcon /> : <SystemIcon />}
<span className="capitalize">{mode}</span>
</button>
))}
Expand Down Expand Up @@ -181,19 +189,6 @@ const ExportIcon = () => (
</svg>
);

const SunIcon = () => (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25M12 18.75V21M3 12h2.25M18.75 12H21M5.636 5.636l1.591 1.591M16.773 16.773l1.591 1.591M5.636 18.364l1.591-1.591M16.773 7.227l1.591-1.591" />
<circle cx="12" cy="12" r="3.25" />
</svg>
);

const MoonIcon = () => (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 12.79A9 9 0 1111.21 3c-.18.57-.21 1.19-.21 1.82A8 8 0 0019.18 13c.63 0 1.25-.03 1.82-.21z" />
</svg>
);

const FileTreeMenuIcon = () => (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/components/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const ActionMenu: React.FC<ActionMenuProps> = ({
})}

{isOpen && (
<div className={panelClassName ?? 'absolute top-full right-0 mt-1 w-56 rounded-lg border border-border bg-popover py-1 shadow-xl z-[70]'}>
<div className={panelClassName ?? 'absolute top-full right-0 mt-1 w-64 rounded-lg border border-border bg-popover py-1 shadow-xl z-[70]'}>
{children({ closeMenu: () => setIsOpen(false) })}
</div>
)}
Expand Down
18 changes: 14 additions & 4 deletions packages/ui/components/PlanHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from './ActionMenu';
import { useTheme } from './ThemeProvider';
import { SunIcon, MoonIcon, SystemIcon } from './icons/themeIcons';
import { getUnsupportedMode } from '../utils/themeRegistry';
import { ReviewAgentsIcon } from './ReviewAgentsIcon';
import { MenuVersionSection } from './MenuVersionSection';
import { TextShimmer } from './TextShimmer';
Expand Down Expand Up @@ -58,7 +59,12 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
bearConfigured,
octarineConfigured,
}) => {
const { theme, setTheme } = useTheme();
const { theme, setTheme, colorTheme } = useTheme();
const unsupportedMode = getUnsupportedMode(colorTheme);
// When the palette can't render the stored mode, the UI is forced to the
// opposite — highlight the mode actually shown instead of the dead one.
const highlightedTheme =
theme === unsupportedMode ? (unsupportedMode === 'light' ? 'dark' : 'light') : theme;

const showUpdateDot = !!updateInfo?.updateAvailable && !updateInfo.dismissed;

Expand Down Expand Up @@ -104,14 +110,18 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
{(['light', 'dark', 'system'] as const).map((mode) => (
<button
key={mode}
disabled={mode === unsupportedMode}
title={mode === unsupportedMode ? 'Not supported by the current color theme' : undefined}
onClick={() => {
closeMenu();
setTheme(mode);
}}
className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium transition-colors ${
theme === mode
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
mode === unsupportedMode
? 'text-muted-foreground opacity-40 cursor-not-allowed'
: highlightedTheme === mode
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{mode === 'light' ? <SunIcon /> : mode === 'dark' ? <MoonIcon /> : <SystemIcon />}
Expand Down
13 changes: 13 additions & 0 deletions packages/ui/utils/themeRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,16 @@ export const BUILT_IN_THEMES: ThemeInfo[] = [
},
},
];

/**
* The mode a color theme cannot render, or null if it supports both.
* Dark-only palettes silently ignore light mode (and vice versa) in
* ThemeProvider — callers use this to disable the incompatible mode
* button instead of letting the toggle no-op.
*/
export function getUnsupportedMode(themeId: string): 'light' | 'dark' | null {
const info = BUILT_IN_THEMES.find(t => t.id === themeId);
if (info?.modeSupport === 'dark-only') return 'light';
if (info?.modeSupport === 'light-only') return 'dark';
return null;
}