[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
212
components/TopTabsQuickControls.tsx
Normal file
212
components/TopTabsQuickControls.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user