* feat(terminal): add per-mode follow-theme resolver and storage keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(terminal): persist per-mode follow-theme selections in settings state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sync): include per-mode follow terminal themes in cloud sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * i18n(terminal): add per-mode follow-theme picker strings * feat(terminal): add type filter and auto option to theme picker * feat(terminal): pick dark/light terminal theme when following app theme Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(terminal): don't flag the auto sentinel as a missing theme in the picker The per-mode follow-theme pickers default to the 'auto' sentinel, which is not a real theme id, so ThemeList's deletedSelectedTheme check classified it as a deleted custom theme and rendered a spurious "Missing Theme" banner above the Auto entry on first open. Exclude TERMINAL_THEME_AUTO from that check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n(terminal): align zh-CN dark/light wording with app convention Use 深色/浅色 (matching the theme picker section headers and global appearance settings) instead of 暗色/亮色 for the per-mode terminal theme labels, so the picker modal reads consistently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(terminal): match follow-theme preview fallback to runtime resolution The per-mode preview memos fell back straight to TERMINAL_THEMES[0] when a selection resolved to a deleted theme, while the runtime currentTerminalTheme memo falls back to the manual terminalThemeId first. Mirror the runtime chain so the Settings preview matches the actual terminal for users with a non-default manual theme. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
/**
|
|
* Theme Select Modal
|
|
* A modal dialog for selecting terminal themes in settings
|
|
*/
|
|
|
|
import React, { useCallback } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { Palette, X } from 'lucide-react';
|
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
|
import { Button } from '../ui/button';
|
|
import { ThemeList } from '../ThemeList';
|
|
|
|
interface ThemeSelectModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
selectedThemeId: string;
|
|
onSelect: (themeId: string) => void;
|
|
filterType?: 'dark' | 'light';
|
|
showAutoOption?: boolean;
|
|
}
|
|
|
|
export const ThemeSelectModal: React.FC<ThemeSelectModalProps> = ({
|
|
open,
|
|
onClose,
|
|
selectedThemeId,
|
|
onSelect,
|
|
filterType,
|
|
showAutoOption,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
|
|
// Handle theme selection - select and close
|
|
const handleThemeSelect = useCallback((themeId: string) => {
|
|
onSelect(themeId);
|
|
onClose();
|
|
}, [onSelect, onClose]);
|
|
|
|
// Handle ESC key
|
|
React.useEffect(() => {
|
|
if (!open) return;
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [open, onClose]);
|
|
|
|
// Handle backdrop click
|
|
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
}, [onClose]);
|
|
|
|
if (!open) return null;
|
|
|
|
const modalTitleId = 'theme-select-modal-title';
|
|
|
|
const modalContent = (
|
|
<div
|
|
className="fixed inset-0 flex items-center justify-center bg-black/60"
|
|
style={{ zIndex: 99999 }}
|
|
onClick={handleBackdropClick}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby={modalTitleId}
|
|
>
|
|
<div
|
|
className="w-[480px] max-h-[600px] bg-background border border-border rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-5 py-3 shrink-0 border-b border-border">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-primary/10">
|
|
<Palette size={16} className="text-primary" />
|
|
</div>
|
|
<h2 id={modalTitleId} className="text-sm font-semibold text-foreground">{t('settings.terminal.themeModal.title')}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="w-8 h-8 rounded-lg flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
aria-label={t('common.close')}
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Theme List */}
|
|
<div className="flex-1 min-h-0 overflow-y-auto p-4">
|
|
<ThemeList
|
|
selectedThemeId={selectedThemeId}
|
|
onSelect={handleThemeSelect}
|
|
filterType={filterType}
|
|
showAutoOption={showAutoOption}
|
|
/>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex justify-end px-5 py-3 shrink-0 border-t border-border bg-muted/20">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={onClose}
|
|
>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// Use Portal to render at document root
|
|
return createPortal(modalContent, document.body);
|
|
};
|
|
|
|
export default ThemeSelectModal;
|