Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
213 lines
8.1 KiB
TypeScript
213 lines
8.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
ChevronDown,
|
|
Droplets,
|
|
Monitor,
|
|
Moon,
|
|
Plug,
|
|
SlidersHorizontal,
|
|
Sun,
|
|
} from 'lucide-react';
|
|
import { useI18n } from '../application/i18n/I18nProvider';
|
|
import { cn } from '../lib/utils';
|
|
import { Button } from './ui/button';
|
|
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
|
import { Switch } from './ui/switch';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
|
|
|
const OPACITY_PRESETS = [
|
|
{ label: '100%', value: 1 },
|
|
{ label: '85%', value: 0.85 },
|
|
{ label: '70%', value: 0.7 },
|
|
] as const;
|
|
|
|
export interface TopTabsQuickControlsProps {
|
|
theme: 'dark' | 'light';
|
|
themePreference: 'dark' | 'light' | 'system';
|
|
onThemeChange: (theme: 'dark' | 'light' | 'system') => void;
|
|
externalMcpEnabled: boolean;
|
|
onToggleExternalMcp: (enabled: boolean) => void;
|
|
showExternalMcpToggle?: boolean;
|
|
windowOpacity: number;
|
|
setWindowOpacity: (opacity: number) => void;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
}
|
|
|
|
export const TopTabsQuickControls: React.FC<TopTabsQuickControlsProps> = ({
|
|
theme,
|
|
themePreference,
|
|
onThemeChange,
|
|
externalMcpEnabled,
|
|
onToggleExternalMcp,
|
|
showExternalMcpToggle = true,
|
|
windowOpacity,
|
|
setWindowOpacity,
|
|
className,
|
|
style,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isOpacityExpanded, setIsOpacityExpanded] = useState(false);
|
|
const opacityPercent = Math.round(windowOpacity * 100);
|
|
const isPresetActive = (value: number) => Math.round(value * 100) === opacityPercent;
|
|
const isDark = theme === 'dark';
|
|
const externalMcpLabelId = React.useId();
|
|
const themeOptions = [
|
|
{ value: 'light' as const, label: t('topTabs.controlPanel.theme.light') },
|
|
{ value: 'dark' as const, label: t('topTabs.controlPanel.theme.dark') },
|
|
{ value: 'system' as const, label: t('topTabs.controlPanel.theme.system') },
|
|
];
|
|
|
|
return (
|
|
<Popover
|
|
open={isOpen}
|
|
onOpenChange={(open) => {
|
|
setIsOpen(open);
|
|
if (!open) setIsOpacityExpanded(false);
|
|
}}
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn('h-7 w-7 shrink-0 app-no-drag top-tab-utility-btn', className)}
|
|
style={{
|
|
...style,
|
|
color: style?.color ?? 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
|
|
}}
|
|
aria-label={t('topTabs.controlPanel')}
|
|
data-section="top-tabs-quick-controls"
|
|
>
|
|
<SlidersHorizontal size={16} />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('topTabs.controlPanel')}</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<PopoverContent
|
|
className="w-72 p-0 app-no-drag"
|
|
align="end"
|
|
sideOffset={6}
|
|
>
|
|
<div className="px-3 py-2 border-b border-border/60">
|
|
<div className="text-sm font-medium">{t('topTabs.controlPanel')}</div>
|
|
</div>
|
|
|
|
<div className="p-2">
|
|
<div>
|
|
<div className="rounded-md">
|
|
<button
|
|
type="button"
|
|
className="flex w-full items-center justify-between gap-3 rounded-md px-2 py-2 text-left hover:bg-muted/40"
|
|
aria-expanded={isOpacityExpanded}
|
|
onClick={() => setIsOpacityExpanded((current) => !current)}
|
|
>
|
|
<div className="flex min-w-0 items-center gap-2 text-sm">
|
|
<Droplets size={14} className="shrink-0 text-muted-foreground" />
|
|
<span className="truncate">{t('topTabs.windowOpacity')}</span>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
<span className="text-xs tabular-nums text-muted-foreground">{opacityPercent}%</span>
|
|
<ChevronDown
|
|
size={14}
|
|
className={cn(
|
|
'text-muted-foreground transition-transform',
|
|
isOpacityExpanded && 'rotate-180',
|
|
)}
|
|
/>
|
|
</div>
|
|
</button>
|
|
|
|
{isOpacityExpanded ? (
|
|
<div className="space-y-2 px-2 pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="range"
|
|
min={50}
|
|
max={100}
|
|
step={1}
|
|
value={opacityPercent}
|
|
onChange={(event) => setWindowOpacity(Number(event.target.value) / 100)}
|
|
className="w-full accent-primary"
|
|
aria-label={t('topTabs.windowOpacity')}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-1" role="group" aria-label={t('topTabs.windowOpacity')}>
|
|
{OPACITY_PRESETS.map((preset) => (
|
|
<button
|
|
key={preset.label}
|
|
type="button"
|
|
onClick={() => setWindowOpacity(preset.value)}
|
|
aria-pressed={isPresetActive(preset.value)}
|
|
className={cn(
|
|
'h-6 flex-1 rounded-md text-[11px] font-medium transition-colors',
|
|
isPresetActive(preset.value)
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
)}
|
|
>
|
|
{preset.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-3 rounded-md px-2 py-2 hover:bg-muted/40">
|
|
<div className="flex min-w-0 items-center gap-2 text-sm">
|
|
{themePreference === 'system'
|
|
? <Monitor size={14} className="shrink-0 text-muted-foreground" />
|
|
: isDark
|
|
? <Moon size={14} className="shrink-0 text-muted-foreground" />
|
|
: <Sun size={14} className="shrink-0 text-muted-foreground" />}
|
|
<span className="truncate">{t('topTabs.controlPanel.theme')}</span>
|
|
</div>
|
|
<div className="flex shrink-0 items-center rounded-md bg-muted/50 p-0.5" role="group" aria-label={t('topTabs.controlPanel.theme')}>
|
|
{themeOptions.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
aria-pressed={themePreference === option.value}
|
|
onClick={() => onThemeChange(option.value)}
|
|
className={cn(
|
|
'h-5 rounded px-1.5 text-[10px] font-medium transition-colors',
|
|
themePreference === option.value
|
|
? 'bg-background text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground',
|
|
)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{showExternalMcpToggle ? (
|
|
<div className="mt-1 border-t border-border/60 pt-1">
|
|
<div className="flex items-center justify-between gap-3 rounded-md px-2 py-2 hover:bg-muted/40">
|
|
<div className="flex min-w-0 items-center gap-2 text-sm">
|
|
<Plug size={14} className="shrink-0 text-muted-foreground" />
|
|
<span id={externalMcpLabelId} className="truncate">{t('topTabs.controlPanel.externalMcp')}</span>
|
|
</div>
|
|
<Switch
|
|
checked={externalMcpEnabled}
|
|
onCheckedChange={onToggleExternalMcp}
|
|
aria-labelledby={externalMcpLabelId}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
export default TopTabsQuickControls;
|