Skip to content
Open
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
33 changes: 9 additions & 24 deletions packages/review-editor/components/ReviewHeaderMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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 { 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';
Expand Down Expand Up @@ -44,10 +45,7 @@ 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 } = useTheme();

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

Expand Down Expand Up @@ -87,21 +85,21 @@ 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) => (
{THEME_MODES.map(({ id, label, Icon }) => (
<button
key={mode}
key={id}
onClick={() => {
closeMenu();
setTheme(mode);
setTheme(id);
}}
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
theme === id
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{mode === 'light' ? <SunIcon /> : <MoonIcon />}
<span className="capitalize">{mode}</span>
<Icon />
<span>{label}</span>
</button>
))}
</div>
Expand Down Expand Up @@ -213,19 +211,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
13 changes: 7 additions & 6 deletions packages/ui/components/ModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -48,17 +49,17 @@ export function ModeToggle() {

{isOpen && (
<div className="absolute right-0 mt-1 w-32 rounded-lg border border-border bg-popover shadow-xl z-50 overflow-hidden py-1">
{(['light', 'dark', 'system'] as const).map((t) => (
{THEME_MODES.map(({ id, label }) => (
<button
key={t}
onClick={() => { setTheme(t); setIsOpen(false); }}
className={`w-full px-3 py-1.5 text-left text-xs capitalize transition-colors ${
theme === t
key={id}
onClick={() => { setTheme(id); setIsOpen(false); }}
className={`w-full px-3 py-1.5 text-left text-xs transition-colors ${
theme === id
? 'text-primary bg-primary/10 font-medium'
: 'text-popover-foreground hover:bg-muted'
}`}
>
{t}
{label}
</button>
))}
</div>
Expand Down
14 changes: 7 additions & 7 deletions packages/ui/components/PlanHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -101,21 +101,21 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
<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', 'system'] as const).map((mode) => (
{THEME_MODES.map(({ id, label, Icon }) => (
<button
key={mode}
key={id}
onClick={() => {
closeMenu();
setTheme(mode);
setTheme(id);
}}
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
theme === id
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{mode === 'light' ? <SunIcon /> : mode === 'dark' ? <MoonIcon /> : <SystemIcon />}
<span className="capitalize">{mode}</span>
<Icon />
<span>{label}</span>
</button>
))}
</div>
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down
53 changes: 18 additions & 35 deletions packages/ui/components/ThemeTab.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,39 +16,22 @@ export const ThemeTab: React.FC<ThemeTabProps> = ({ onPreview, compact }) => {
<div className={compact ? 'flex items-center gap-3 mb-2' : 'space-y-2'}>
{!compact && <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Mode</label>}
<div className="flex gap-1">
{(['dark', 'light', 'system'] as Mode[]).map(m => {
const isActive = mode === m;
return (
<button
key={m}
onClick={() => setMode(m)}
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
isActive
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground'
}`}
>
{m === 'dark' && (
<span className="flex items-center gap-1.5">
<MoonIcon className="w-3 h-3" />
Dark
</span>
)}
{m === 'light' && (
<span className="flex items-center gap-1.5">
<SunIcon className="w-3 h-3" />
Light
</span>
)}
{m === 'system' && (
<span className="flex items-center gap-1.5">
<SystemIcon className="w-3 h-3" />
System
</span>
)}
</button>
);
})}
{THEME_MODES.map(({ id, label, Icon }) => (
<button
key={id}
onClick={() => setMode(id)}
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-colors ${
mode === id
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground'
}`}
>
<span className="flex items-center gap-1.5">
<Icon className="w-3 h-3" />
{label}
</span>
</button>
))}
</div>
{compact && (
<span className="text-[10px] text-muted-foreground/60 ml-auto flex items-center gap-1">
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/components/icons/themeIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
23 changes: 23 additions & 0 deletions packages/ui/components/themeModes.ts
Original file line number Diff line number Diff line change
@@ -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<IconProps>;
}

/** 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'];