mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
refactor(ui): consolidate toggle switches
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { SwitchIndicator } from '@/components/ui/switch'
|
||||
import { SettingsSwitch } from '@/components/settings/SettingsFormControls'
|
||||
import type RepoCombobox from '@/components/repo/RepoCombobox'
|
||||
import AgentCombobox from '@/components/agent/AgentCombobox'
|
||||
@@ -1209,20 +1210,7 @@ export default function NewWorkspaceComposerCard({
|
||||
onClick={() => onCreateMultipleChange?.(!createMultiple)}
|
||||
className="group flex w-fit cursor-pointer items-center gap-2 rounded-md text-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors',
|
||||
createMultiple ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
createMultiple ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<SwitchIndicator checked={createMultiple} />
|
||||
<span className="text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
{translate('auto.components.NewWorkspaceComposerCard.createMultiple', 'Create more')}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSX } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { ContextualTourStepControl } from '../../../../shared/contextual-tours'
|
||||
import { CONTEXTUAL_TOUR_ENABLE_AUTO_WORKSPACE_NAME_EVENT } from './contextual-tour-composer-events'
|
||||
@@ -43,33 +43,20 @@ function AutoRenameBranchFromWorkControl(): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
<Switch
|
||||
checked={enabled}
|
||||
aria-label={translate(
|
||||
'auto.components.contextual.tours.ContextualTourControl.186eecc34f',
|
||||
'Auto-name workspace from first agent message'
|
||||
)}
|
||||
onClick={() => {
|
||||
onCheckedChange={() => {
|
||||
toggleAutoRenameBranchFromWork({
|
||||
enabled,
|
||||
updateSettings,
|
||||
dispatchEvent: (event) => window.dispatchEvent(event)
|
||||
})
|
||||
}}
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors',
|
||||
enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
enabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSX } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
||||
type AiCommitPrSettingsSwitchProps = {
|
||||
checked: boolean
|
||||
@@ -12,24 +12,5 @@ export function AiCommitPrSettingsSwitch({
|
||||
label,
|
||||
onToggle
|
||||
}: AiCommitPrSettingsSwitchProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-label={label}
|
||||
aria-checked={checked}
|
||||
onClick={onToggle}
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors',
|
||||
checked ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
checked ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
return <Switch aria-label={label} checked={checked} onCheckedChange={onToggle} />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSX } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { getAgentAwakeDescription, getAgentAwakeTitle } from '../settings/agent-awake-copy'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -33,23 +33,13 @@ export function KeepAwakeCard(props: {
|
||||
{getAgentAwakeDescription()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
<Switch
|
||||
aria-label={title}
|
||||
aria-checked={enabled}
|
||||
onClick={() => updateSettings(computerAwakeSettingsForMode(enabled ? 'off' : 'auto'))}
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors',
|
||||
enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
enabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings(computerAwakeSettingsForMode(checked ? 'auto' : 'off'))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Label } from '../ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { Switch } from '../ui/switch'
|
||||
import {
|
||||
AlertTriangle,
|
||||
ExternalLink,
|
||||
@@ -1555,25 +1556,15 @@ export function AccountsPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.geminiCliOAuthEnabled}
|
||||
onClick={() => {
|
||||
<Switch
|
||||
checked={settings.geminiCliOAuthEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
recordFeatureInteraction('usage-tracking')
|
||||
updateSettings({
|
||||
geminiCliOAuthEnabled: !settings.geminiCliOAuthEnabled
|
||||
geminiCliOAuthEnabled: checked
|
||||
})
|
||||
}}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.geminiCliOAuthEnabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.geminiCliOAuthEnabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</section>
|
||||
) : null,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SourceControlActionVariableChips } from '../source-control/SourceContro
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'
|
||||
import { Label } from '../ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { getAutoRenameBranchAdvancedSearchEntries } from './auto-rename-branch-search'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search'
|
||||
@@ -172,24 +173,14 @@ export function AutoRenameBranchFromWorkSetting({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.autoRenameBranchFromWork}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.autoRenameBranchFromWork}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
autoRenameBranchFromWork: !settings.autoRenameBranchFromWork
|
||||
autoRenameBranchFromWork: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.autoRenameBranchFromWork ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.autoRenameBranchFromWork ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collapsible open={advancedOpen} onOpenChange={setOptionsOpen}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
@@ -39,25 +40,15 @@ export function BrowserLinkRoutingSetting({
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">{linkRoutingDescription}</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.openLinksInApp}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.openLinksInApp}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
openLinksInApp: !settings.openLinksInApp,
|
||||
openLinksInApp: checked,
|
||||
openLinksInAppPreferencePrompted: true
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.openLinksInApp ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.openLinksInApp ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
export function BrowserUseEnableSwitch({
|
||||
enabled,
|
||||
onToggle
|
||||
@@ -7,23 +8,13 @@ export function BrowserUseEnableSwitch({
|
||||
onToggle: () => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
<Switch
|
||||
checked={enabled}
|
||||
aria-label={translate(
|
||||
'auto.components.settings.BrowserUseEnableSwitch.aea3f45349',
|
||||
'Enable Agent Browser Use'
|
||||
)}
|
||||
onClick={onToggle}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
|
||||
enabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={onToggle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { Button } from '../ui/button'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
|
||||
import { CliRegistrationDialog } from './CliRegistrationDialog'
|
||||
@@ -288,21 +289,12 @@ export function CliSection({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{!isBrowserManaged ? (
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
disabled={loading || !isSupported || pathStatusUnknown || busyAction !== null}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors ${
|
||||
isEnabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
} ${loading || !isSupported || pathStatusUnknown || busyAction !== null ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
isEnabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={() => setDialogOpen(true)}
|
||||
className="disabled:opacity-60"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getRuntimeGitScope } from '../../runtime/runtime-git-client'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Input } from '../ui/input'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { SourceControlAiActionRecipeDefaults } from './SourceControlAiActionRecipeDefaults'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
@@ -199,20 +200,7 @@ export function CommitMessageAiPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={config.enabled}
|
||||
onClick={onToggleEnabled}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
config.enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
config.enabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<Switch checked={config.enabled} onCheckedChange={onToggleEnabled} />
|
||||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { useAppStore } from '../../store'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
@@ -86,23 +87,12 @@ export function ExperimentalPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={settings.experimentalPet}
|
||||
onClick={() => {
|
||||
updateSettings({ experimentalPet: !settings.experimentalPet })
|
||||
<Switch
|
||||
checked={settings.experimentalPet}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSettings({ experimentalPet: checked })
|
||||
}}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.experimentalPet ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.experimentalPet ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
@@ -129,25 +119,14 @@ export function ExperimentalPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={settings.experimentalActivity}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.experimentalActivity}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
experimentalActivity: !settings.experimentalActivity
|
||||
experimentalActivity: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.experimentalActivity ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.experimentalActivity ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
@@ -188,25 +167,14 @@ export function ExperimentalPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={settings.experimentalTerminalAttention}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.experimentalTerminalAttention}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
experimentalTerminalAttention: !settings.experimentalTerminalAttention
|
||||
experimentalTerminalAttention: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.experimentalTerminalAttention ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.experimentalTerminalAttention ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SourceControlAiSettingsPatch } from '../../../../shared/source-con
|
||||
import { DEFAULT_SOURCE_CONTROL_GROUP_ORDER } from '../../../../shared/source-control-group-order'
|
||||
import { Input } from '../ui/input'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { useAppStore } from '../../store'
|
||||
import { getGitPaneSearchEntries } from './git-search'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
@@ -275,26 +276,14 @@ export function GitPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.refreshLocalBaseRefOnWorktreeCreate}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.refreshLocalBaseRefOnWorktreeCreate}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
refreshLocalBaseRefOnWorktreeCreate: !settings.refreshLocalBaseRefOnWorktreeCreate
|
||||
refreshLocalBaseRefOnWorktreeCreate: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.refreshLocalBaseRefOnWorktreeCreate
|
||||
? 'bg-foreground'
|
||||
: 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.refreshLocalBaseRefOnWorktreeCreate ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, {
|
||||
@@ -371,24 +360,14 @@ export function GitPane({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.enableGitHubAttribution}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.enableGitHubAttribution}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
enableGitHubAttribution: !settings.enableGitHubAttribution
|
||||
enableGitHubAttribution: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.enableGitHubAttribution ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.enableGitHubAttribution ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</SearchableSetting>
|
||||
) : null
|
||||
].filter(Boolean)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
// Why: anything in this group is deliberately unfinished or staff-only. The
|
||||
@@ -38,17 +39,16 @@ export function HiddenExperimentalGroup(): React.JSX.Element {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<Switch
|
||||
aria-label={translate(
|
||||
'auto.components.settings.HiddenExperimentalGroup.d0f914a528',
|
||||
'Placeholder toggle'
|
||||
)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-not-allowed items-center rounded-full border border-orange-500/40 bg-orange-500/20 opacity-70"
|
||||
checked={false}
|
||||
className="border-orange-500/40 data-[state=unchecked]:bg-orange-500/20 disabled:opacity-70"
|
||||
disabled
|
||||
>
|
||||
<span className="inline-block h-3.5 w-3.5 translate-x-0.5 transform rounded-full bg-orange-200 shadow-sm dark:bg-orange-100" />
|
||||
</button>
|
||||
thumbClassName="data-[state=unchecked]:bg-orange-200 dark:data-[state=unchecked]:bg-orange-100"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { isDefaultPrimarySelectionMiddleClickPasteUserAgent } from '@/hooks/usePrimarySelectionPaste'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -55,25 +56,14 @@ export function InputPane({ settings, updateSettings }: InputPaneProps): React.J
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
primarySelectionMiddleClickPaste: !enabled
|
||||
primarySelectionMiddleClickPaste: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
enabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
|
||||
export type NotificationSettingToggleProps = {
|
||||
label: string
|
||||
@@ -27,22 +28,7 @@ export function NotificationSettingToggle({
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
disabled={disabled}
|
||||
onClick={onToggle}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors ${
|
||||
checked ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
checked ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<Switch checked={checked} aria-label={label} disabled={disabled} onCheckedChange={onToggle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,9 @@ describe('PrivacyPane — computeBlockedReason', () => {
|
||||
})
|
||||
|
||||
describe('PrivacyPane — markup respects blocked state', () => {
|
||||
const switchTag = (markup: string): string =>
|
||||
markup.match(/<button[^>]*role="switch"[^>]*>/)?.[0] ?? ''
|
||||
|
||||
// The useEffect that fetches consent does not run in renderToStaticMarkup
|
||||
// (no DOM, no state flushes), so the rendered toggle always reads the
|
||||
// `consent === null` branch of the effect. Env-var blocked-state markup
|
||||
@@ -189,9 +192,7 @@ describe('PrivacyPane — markup respects blocked state', () => {
|
||||
})
|
||||
})
|
||||
)
|
||||
// Negative assertion: the disabled attribute should not appear on the
|
||||
// switch. renderToStaticMarkup elides `disabled={false}` entirely.
|
||||
expect(markup).not.toMatch(/role="switch"[^>]*disabled/)
|
||||
expect(switchTag(markup)).not.toContain(' disabled=""')
|
||||
})
|
||||
|
||||
it('leaves the toggle enabled for an existing user awaiting the banner (flip dismisses the banner)', () => {
|
||||
@@ -206,7 +207,7 @@ describe('PrivacyPane — markup respects blocked state', () => {
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(markup).not.toMatch(/role="switch"[^>]*disabled/)
|
||||
expect(switchTag(markup)).not.toContain(' disabled=""')
|
||||
})
|
||||
|
||||
it('leaves the toggle enabled once the existing-user banner is resolved', () => {
|
||||
@@ -218,6 +219,6 @@ describe('PrivacyPane — markup respects blocked state', () => {
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(markup).not.toMatch(/role="switch"[^>]*disabled/)
|
||||
expect(switchTag(markup)).not.toContain(' disabled=""')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import type { TelemetryConsentState } from '../../../../shared/telemetry-consent-types'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { PRIVACY_URL, getConsentState, setOptIn as telemetrySetOptIn } from '../../lib/telemetry'
|
||||
import { useAppStore } from '../../store'
|
||||
import { PrivacyDiagnosticsSection } from './PrivacyDiagnosticsSection'
|
||||
@@ -111,26 +112,16 @@ export function PrivacyPane({ settings }: PrivacyPaneProps): React.JSX.Element {
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={toggleChecked}
|
||||
<Switch
|
||||
checked={toggleChecked}
|
||||
aria-label={translate(
|
||||
'auto.components.settings.PrivacyPane.fe904ac984',
|
||||
'Share anonymous usage data'
|
||||
)}
|
||||
aria-describedby={blocked ? PRIVACY_PANE_BLOCKED_HELPER_ID : undefined}
|
||||
disabled={blocked !== null || inFlight}
|
||||
onClick={handleToggle}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors ${
|
||||
toggleChecked ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
} ${blocked !== null || inFlight ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
toggleChecked ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={() => void handleToggle()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{blocked ? <BlockedHelper blocked={blocked} id={PRIVACY_PANE_BLOCKED_HELPER_ID} /> : null}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '../ui/input'
|
||||
import { Label } from '../ui/label'
|
||||
import { Popover, PopoverAnchor, PopoverContent } from '../ui/popover'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { Check, ChevronsUpDown, CircleX } from 'lucide-react'
|
||||
import { normalizeColor, type TerminalThemeOption } from '@/lib/terminal-theme'
|
||||
import { MAX_THEME_RESULTS } from './SettingsConstants'
|
||||
@@ -36,26 +37,13 @@ export function SettingsSwitch({
|
||||
disabled
|
||||
}: SettingsSwitchProps): React.JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
<Switch
|
||||
checked={checked}
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
disabled={disabled}
|
||||
onClick={onChange}
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
checked ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
checked ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RotateCw } from 'lucide-react'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { Button } from '../ui/button'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { ColorField, NumberField } from './SettingsFormControls'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { clampNumber } from '@/lib/terminal-theme'
|
||||
@@ -119,24 +120,10 @@ export function TerminalWindowSection({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.windowBackgroundBlur ?? false}
|
||||
onClick={() =>
|
||||
updateSettings({ windowBackgroundBlur: !settings.windowBackgroundBlur })
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
(settings.windowBackgroundBlur ?? false)
|
||||
? 'bg-foreground'
|
||||
: 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
(settings.windowBackgroundBlur ?? false) ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<Switch
|
||||
checked={settings.windowBackgroundBlur ?? false}
|
||||
onCheckedChange={(checked) => updateSettings({ windowBackgroundBlur: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{blurPendingRestart ? (
|
||||
@@ -253,28 +240,14 @@ export function TerminalWindowSection({
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.terminalMouseHideWhileTyping ?? false}
|
||||
onClick={() =>
|
||||
<Switch
|
||||
checked={settings.terminalMouseHideWhileTyping ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
terminalMouseHideWhileTyping: !settings.terminalMouseHideWhileTyping
|
||||
terminalMouseHideWhileTyping: checked
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
(settings.terminalMouseHideWhileTyping ?? false)
|
||||
? 'bg-foreground'
|
||||
: 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
(settings.terminalMouseHideWhileTyping ?? false)
|
||||
? 'translate-x-4.5'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { VoiceSettings } from '../../../../shared/speech-types'
|
||||
import { Label } from '../ui/label'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { VoiceMicrophoneSetting } from './VoiceMicrophoneSetting'
|
||||
@@ -35,26 +36,17 @@ export function VoiceDictationSettingsSection({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={voiceSettings.enabled}
|
||||
<Switch
|
||||
checked={voiceSettings.enabled}
|
||||
aria-label={translate(
|
||||
'auto.components.settings.VoicePane.0121960365',
|
||||
'Enable Voice Dictation'
|
||||
)}
|
||||
aria-busy={permissionPending}
|
||||
disabled={permissionPending}
|
||||
onClick={() => void onToggleVoiceDictation()}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
voiceSettings.enabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
} ${permissionPending ? 'cursor-wait opacity-70' : ''}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
voiceSettings.enabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={() => void onToggleVoiceDictation()}
|
||||
className="disabled:cursor-wait disabled:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DialogTitle
|
||||
} from '../ui/dialog'
|
||||
import { Label } from '../ui/label'
|
||||
import { Switch } from '../ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
@@ -187,21 +188,12 @@ export function WslCliRegistration({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
disabled={loading || !isSupported || busyAction !== null}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors ${
|
||||
isEnabled ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
} ${loading || !isSupported || busyAction !== null ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
isEnabled ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={() => setDialogOpen(true)}
|
||||
className="disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { DropdownMenuShortcut } from '@/components/ui/dropdown-menu'
|
||||
import { SwitchIndicator } from '@/components/ui/switch'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
@@ -51,20 +52,11 @@ export function FilterToggleRow({
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{shortcutLabel ? <DropdownMenuShortcut>{shortcutLabel}</DropdownMenuShortcut> : null}
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'relative h-3.5 w-6 shrink-0 rounded-full transition-colors',
|
||||
checked ? 'bg-primary' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-0.5 left-0.5 size-2.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
checked && 'translate-x-2.5'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<SwitchIndicator
|
||||
checked={checked}
|
||||
size="compact"
|
||||
className="data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SwitchIndicator } from '@/components/ui/switch'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getAllWorktreesFromState } from '@/store/selectors'
|
||||
@@ -241,20 +242,7 @@ export function HostRemoveDialog({
|
||||
onClick={() => setDeleteWorkspaces((v) => !v)}
|
||||
className="group mt-0.5 flex shrink-0 cursor-pointer items-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors',
|
||||
deleteWorkspaces ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform',
|
||||
deleteWorkspaces ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<SwitchIndicator checked={deleteWorkspaces} />
|
||||
</button>
|
||||
<span className="min-w-0 flex-1 text-xs leading-snug">
|
||||
<span className="font-medium text-foreground">{deleteOptionLabel}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import { SwitchIndicator } from '../ui/switch'
|
||||
|
||||
type ClaudeUsageLoadingStateProps = {
|
||||
title?: string
|
||||
@@ -20,9 +21,11 @@ export function ClaudeUsageLoadingState({
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 self-start">
|
||||
<RefreshCw className="size-3.5 animate-spin text-muted-foreground" />
|
||||
<div className="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent bg-foreground/80">
|
||||
<span className="pointer-events-none block size-3.5 translate-x-4.5 rounded-full bg-background shadow-sm" />
|
||||
</div>
|
||||
<SwitchIndicator
|
||||
checked
|
||||
className="data-[state=checked]:bg-foreground/80"
|
||||
thumbClassName="transition-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import type { ClaudeUsageRange, ClaudeUsageScope } from '../../../../shared/claude-usage-types'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Button } from '../ui/button'
|
||||
import { Switch } from '../ui/switch'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -103,19 +104,14 @@ export function ClaudeUsagePane(): React.JSX.Element {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={false}
|
||||
<Switch
|
||||
checked={false}
|
||||
aria-label={translate(
|
||||
'auto.components.stats.ClaudeUsagePane.424cd50412',
|
||||
'Enable Claude usage analytics'
|
||||
)}
|
||||
onClick={() => handleSetEnabled(true)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
|
||||
>
|
||||
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
|
||||
</button>
|
||||
onCheckedChange={handleSetEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -222,19 +218,14 @@ export function ClaudeUsagePane(): React.JSX.Element {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={true}
|
||||
<Switch
|
||||
checked
|
||||
aria-label={translate(
|
||||
'auto.components.stats.ClaudeUsagePane.424cd50412',
|
||||
'Enable Claude usage analytics'
|
||||
)}
|
||||
onClick={() => handleSetEnabled(false)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
|
||||
>
|
||||
<span className="pointer-events-none block size-3.5 translate-x-4.5 rounded-full bg-background shadow-sm transition-transform" />
|
||||
</button>
|
||||
onCheckedChange={handleSetEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { CodexUsageRange, CodexUsageScope } from '../../../../shared/codex-usage-types'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Button } from '../ui/button'
|
||||
import { Switch } from '../ui/switch'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -99,19 +100,14 @@ export function CodexUsagePane(): React.JSX.Element {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={false}
|
||||
<Switch
|
||||
checked={false}
|
||||
aria-label={translate(
|
||||
'auto.components.stats.CodexUsagePane.f7c1affbd5',
|
||||
'Enable Codex usage analytics'
|
||||
)}
|
||||
onClick={() => handleSetEnabled(true)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
|
||||
>
|
||||
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
|
||||
</button>
|
||||
onCheckedChange={handleSetEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -224,19 +220,14 @@ export function CodexUsagePane(): React.JSX.Element {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={true}
|
||||
<Switch
|
||||
checked
|
||||
aria-label={translate(
|
||||
'auto.components.stats.CodexUsagePane.f7c1affbd5',
|
||||
'Enable Codex usage analytics'
|
||||
)}
|
||||
onClick={() => handleSetEnabled(false)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
|
||||
>
|
||||
<span className="pointer-events-none block size-3.5 translate-x-4.5 rounded-full bg-background shadow-sm transition-transform" />
|
||||
</button>
|
||||
onCheckedChange={handleSetEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from '../../../../shared/opencode-usage-types'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Button } from '../ui/button'
|
||||
import { Switch } from '../ui/switch'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -107,19 +108,14 @@ export function OpenCodeUsagePane(): React.JSX.Element {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={false}
|
||||
<Switch
|
||||
checked={false}
|
||||
aria-label={translate(
|
||||
'auto.components.stats.OpenCodeUsagePane.f04131b3be',
|
||||
'Enable OpenCode usage analytics'
|
||||
)}
|
||||
onClick={() => handleSetEnabled(true)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
|
||||
>
|
||||
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
|
||||
</button>
|
||||
onCheckedChange={handleSetEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -235,19 +231,14 @@ export function OpenCodeUsagePane(): React.JSX.Element {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={true}
|
||||
<Switch
|
||||
checked
|
||||
aria-label={translate(
|
||||
'auto.components.stats.OpenCodeUsagePane.f04131b3be',
|
||||
'Enable OpenCode usage analytics'
|
||||
)}
|
||||
onClick={() => handleSetEnabled(false)}
|
||||
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
|
||||
>
|
||||
<span className="pointer-events-none block size-3.5 translate-x-4.5 rounded-full bg-background shadow-sm transition-transform" />
|
||||
</button>
|
||||
onCheckedChange={handleSetEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+5
-15
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
type TerminalQuickCommandAppendEnterSwitchProps = {
|
||||
appendEnter: boolean
|
||||
onToggle: () => void
|
||||
@@ -24,25 +25,14 @@ export function TerminalQuickCommandAppendEnterSwitch({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={appendEnter}
|
||||
<Switch
|
||||
checked={appendEnter}
|
||||
aria-label={translate(
|
||||
'auto.components.terminal.quick.commands.TerminalQuickCommandAppendEnterSwitch.e4e5fed3b3',
|
||||
'Toggle append Enter'
|
||||
)}
|
||||
onClick={onToggle}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
appendEnter ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
appendEnter ? 'translate-x-4.5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onCheckedChange={onToggle}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { relative, resolve } from 'node:path'
|
||||
// TypeScript 7 is a native CLI; AST tests still need the legacy JavaScript API.
|
||||
import ts from 'typescript-api'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
// Why: the h-5 w-9 switch is hand-rolled at every call site instead of living in a
|
||||
// primitive, so the handle offset drifts. The 1px-bordered track leaves a 34px content
|
||||
// box for the 14px (`size-3.5`) handle, and `translate-x-0.5` insets the off state by
|
||||
// 2px, so a symmetric on state is 34 - 14 - 2 = 18px (`translate-x-4.5`).
|
||||
|
||||
const RENDERER_ROOT = resolve('src/renderer/src')
|
||||
|
||||
const OFF_STATE = 'translate-x-0.5'
|
||||
const ON_STATE = 'translate-x-4.5'
|
||||
|
||||
/** The track geometry the arithmetic above is derived from; other switch sizes are not ours to check. */
|
||||
const TRACK = ['h-5', 'w-9']
|
||||
|
||||
/** The handle's 14px size, in both the shorthand and long-hand spellings in use. */
|
||||
const HANDLE_SIZE = [['size-3.5'], ['h-3.5', 'w-3.5']]
|
||||
|
||||
/** Every current handle is the round knob; this is what separates it from a size-3.5 icon in the same track. */
|
||||
const HANDLE_SHAPE = 'rounded-full'
|
||||
|
||||
/** Any horizontal translate — negative, arbitrary, variant-prefixed or named — so no respelling reads as "no offset". */
|
||||
const OFFSET = /(?:^|:)-?translate-x-\S+$/
|
||||
|
||||
/** Anything that moves the track off the 34px content box the 18px on-state is derived from. */
|
||||
const BORDER_WIDTH = /^border(?:-[xytrbles])?-(?:\d+|\[)/
|
||||
const TRACK_PADDING = /^p[xytrbles]?-(?:\d|\[)/
|
||||
|
||||
function collectSourceFiles(dir: string, files: string[] = []): string[] {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const filePath = resolve(dir, name)
|
||||
if (statSync(filePath).isDirectory()) {
|
||||
collectSourceFiles(filePath, files)
|
||||
} else if (name.endsWith('.tsx') && !name.endsWith('.test.tsx')) {
|
||||
files.push(filePath)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
/** Class tokens anywhere under a node, flattening the ternaries, template holes and `cn()` args a className is built from. */
|
||||
function tokensIn(node: ts.Node): string[] {
|
||||
const tokens: string[] = []
|
||||
const read = (child: ts.Node): void => {
|
||||
if (ts.isStringLiteral(child) || ts.isTemplateLiteralToken(child)) {
|
||||
tokens.push(...child.text.split(/\s+/).filter(Boolean))
|
||||
}
|
||||
ts.forEachChild(child, read)
|
||||
}
|
||||
read(node)
|
||||
return tokens
|
||||
}
|
||||
|
||||
/** Reading the className attribute's own subtree keeps a child element's classes out of its parent's token set. */
|
||||
function classNameOf(element: ts.JsxOpeningLikeElement): ts.Node | undefined {
|
||||
const className = element.attributes.properties.find(
|
||||
(property): property is ts.JsxAttribute =>
|
||||
ts.isJsxAttribute(property) && property.name.getText() === 'className'
|
||||
)
|
||||
return className?.initializer
|
||||
}
|
||||
|
||||
function hasAll(tokens: string[], required: string[]): boolean {
|
||||
return required.every((token) => tokens.includes(token))
|
||||
}
|
||||
|
||||
/**
|
||||
* The offsets each side of a conditional className contributes. A handle that toggles is
|
||||
* two-state by construction, so this tells a deliberately single-state knob apart from one
|
||||
* that lost a branch — which a flat token list cannot.
|
||||
*/
|
||||
function offsetBranches(node: ts.Node): [string[], string[]][] {
|
||||
const branches: [string[], string[]][] = []
|
||||
const read = (child: ts.Node): void => {
|
||||
if (ts.isConditionalExpression(child)) {
|
||||
const whenTrue = tokensIn(child.whenTrue).filter((token) => OFFSET.test(token))
|
||||
const whenFalse = tokensIn(child.whenFalse).filter((token) => OFFSET.test(token))
|
||||
if (whenTrue.length > 0 || whenFalse.length > 0) {
|
||||
branches.push([whenTrue, whenFalse])
|
||||
return
|
||||
}
|
||||
}
|
||||
ts.forEachChild(child, read)
|
||||
}
|
||||
read(node)
|
||||
return branches
|
||||
}
|
||||
|
||||
function trackDrift(tokens: string[]): string | undefined {
|
||||
if (!tokens.includes('border')) {
|
||||
return 'no 1px border'
|
||||
}
|
||||
if (tokens.includes('box-content')) {
|
||||
return 'box-content'
|
||||
}
|
||||
return tokens.find((token) => BORDER_WIDTH.test(token) || TRACK_PADDING.test(token))
|
||||
}
|
||||
|
||||
type Handle = { at: string; offsets: string[] }
|
||||
type Toggle = { at: string; branches: [string[], string[]] }
|
||||
type Scan = { handles: Handle[]; toggles: Toggle[]; drifted: string[]; knobless: string[] }
|
||||
|
||||
/**
|
||||
* Handles are found *inside* a track element rather than by scanning source text forward, so
|
||||
* neither a sibling element nor a same-file icon can be mistaken for the knob.
|
||||
*/
|
||||
function collectFile(filePath: string, source: string, scan: Scan): void {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX
|
||||
)
|
||||
|
||||
const at = (node: ts.Node): string => {
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
||||
return `${relative(RENDERER_ROOT, filePath)}:${line + 1}`
|
||||
}
|
||||
|
||||
const readHandles = (node: ts.Node): void => {
|
||||
if (ts.isJsxOpeningLikeElement(node)) {
|
||||
const className = classNameOf(node)
|
||||
const tokens = className ? tokensIn(className) : []
|
||||
if (
|
||||
className &&
|
||||
HANDLE_SIZE.some((size) => hasAll(tokens, size)) &&
|
||||
tokens.includes(HANDLE_SHAPE)
|
||||
) {
|
||||
scan.handles.push({ at: at(node), offsets: tokens.filter((token) => OFFSET.test(token)) })
|
||||
for (const branches of offsetBranches(className)) {
|
||||
scan.toggles.push({ at: at(node), branches })
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, readHandles)
|
||||
}
|
||||
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isJsxOpeningLikeElement(node)) {
|
||||
const className = classNameOf(node)
|
||||
const tokens = className ? tokensIn(className) : []
|
||||
if (hasAll(tokens, TRACK)) {
|
||||
const drift = trackDrift(tokens)
|
||||
if (drift) {
|
||||
scan.drifted.push(`${at(node)}: ${drift}`)
|
||||
}
|
||||
// The knob lives in the track's subtree; a self-closing track has no subtree to walk.
|
||||
const found = scan.handles.length
|
||||
if (ts.isJsxOpeningElement(node)) {
|
||||
readHandles(node.parent)
|
||||
}
|
||||
// A track whose knob moved into a child component would otherwise leave the scan silently.
|
||||
if (scan.handles.length === found) {
|
||||
scan.knobless.push(at(node))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
}
|
||||
|
||||
let cached: Scan | undefined
|
||||
|
||||
/** Parsing every gated file is the expensive part, so all three assertions share one scan. */
|
||||
function scanRenderer(): Scan {
|
||||
if (!cached) {
|
||||
const scan: Scan = { handles: [], toggles: [], drifted: [], knobless: [] }
|
||||
|
||||
for (const filePath of collectSourceFiles(RENDERER_ROOT)) {
|
||||
const source = readFileSync(filePath, 'utf8')
|
||||
if (TRACK.every((token) => source.includes(token))) {
|
||||
collectFile(filePath, source, scan)
|
||||
}
|
||||
}
|
||||
|
||||
// A track nested inside another h-5 w-9 element would otherwise be collected twice.
|
||||
const seen = new Set<string>()
|
||||
scan.handles = scan.handles.filter((handle) => !seen.has(handle.at) && seen.add(handle.at))
|
||||
cached = scan
|
||||
}
|
||||
|
||||
// A scan that stops finding switches has lost its bounds, not proven the tree clean. The
|
||||
// floor only has to catch a collapse — `knobless` and `drifted` catch the structural cases —
|
||||
// so it sits well under the ~34 handles in the tree rather than pinning the current count.
|
||||
expect(cached.handles.length, 'the switch scan lost its bounds').toBeGreaterThan(25)
|
||||
|
||||
return cached
|
||||
}
|
||||
|
||||
describe('toggle switch handle alignment', () => {
|
||||
it('insets the handle by 2px at both ends of the track', () => {
|
||||
const offenders = scanRenderer().handles.flatMap((handle) =>
|
||||
handle.offsets.length === 0
|
||||
? [`${handle.at}: no translate-x offset`]
|
||||
: handle.offsets
|
||||
.filter((offset) => offset !== OFF_STATE && offset !== ON_STATE)
|
||||
.map((offset) => `${handle.at}: ${offset}`)
|
||||
)
|
||||
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
|
||||
it('gives every toggling handle both ends of the pair', () => {
|
||||
const pairs = (on: string[], off: string[]): boolean =>
|
||||
on.length === 1 && on[0] === ON_STATE && off.length === 1 && off[0] === OFF_STATE
|
||||
|
||||
const offenders = scanRenderer()
|
||||
.toggles.filter(({ branches: [a, b] }) => !pairs(a, b) && !pairs(b, a))
|
||||
.map(
|
||||
({ at, branches }) => `${at}: ${branches.map((side) => side.join(' ') || '—').join(' / ')}`
|
||||
)
|
||||
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps every track on the geometry the 18px on-state is derived from', () => {
|
||||
expect(scanRenderer().drifted).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps every track a knob the scan can see', () => {
|
||||
expect(scanRenderer().knobless).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Switch, SwitchIndicator } from './switch'
|
||||
|
||||
describe('Switch', () => {
|
||||
it('renders the checked state with symmetric track geometry', () => {
|
||||
const html = renderToStaticMarkup(<Switch checked aria-label="Example setting" />)
|
||||
|
||||
expect(html).toContain('role="switch"')
|
||||
expect(html).toContain('aria-checked="true"')
|
||||
expect(html).toContain('p-0.5')
|
||||
expect(html).toContain('data-[state=checked]:translate-x-4')
|
||||
})
|
||||
|
||||
it('renders a visual-only indicator without switch semantics', () => {
|
||||
const html = renderToStaticMarkup(<SwitchIndicator checked={false} />)
|
||||
|
||||
expect(html).toContain('aria-hidden="true"')
|
||||
expect(html).toContain('data-state="unchecked"')
|
||||
expect(html).not.toContain('role="switch"')
|
||||
})
|
||||
|
||||
it('renders compact indicator geometry', () => {
|
||||
const html = renderToStaticMarkup(<SwitchIndicator checked size="compact" />)
|
||||
|
||||
expect(html).toContain('h-3.5 w-6')
|
||||
expect(html).toContain('size-2.5')
|
||||
expect(html).toContain('data-[state=checked]:translate-x-2.5')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react'
|
||||
import { Switch as SwitchPrimitive } from 'radix-ui'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const trackClassName =
|
||||
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent p-0.5 transition-colors data-[state=checked]:bg-foreground data-[state=unchecked]:bg-muted-foreground/30'
|
||||
const thumbClassName =
|
||||
'pointer-events-none block size-3.5 translate-x-0 rounded-full bg-background shadow-sm transition-transform data-[state=checked]:translate-x-4'
|
||||
const compactTrackClassName = 'h-3.5 w-6 border-0 p-0.5'
|
||||
const compactThumbClassName = 'size-2.5 data-[state=checked]:translate-x-2.5'
|
||||
|
||||
type SwitchProps = React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||
thumbClassName?: string
|
||||
}
|
||||
|
||||
function Switch({ className, thumbClassName: thumbClasses, ...props }: SwitchProps) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
trackClassName,
|
||||
'cursor-pointer outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(thumbClassName, thumbClasses)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
type SwitchIndicatorProps = Omit<React.ComponentProps<'span'>, 'children'> & {
|
||||
checked: boolean
|
||||
size?: 'default' | 'compact'
|
||||
thumbClassName?: string
|
||||
}
|
||||
|
||||
function SwitchIndicator({
|
||||
checked,
|
||||
className,
|
||||
size = 'default',
|
||||
thumbClassName: thumbClasses,
|
||||
...props
|
||||
}: SwitchIndicatorProps) {
|
||||
const state = checked ? 'checked' : 'unchecked'
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-slot="switch-indicator"
|
||||
data-state={state}
|
||||
className={cn(trackClassName, size === 'compact' && compactTrackClassName, className)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="switch-thumb"
|
||||
data-state={state}
|
||||
className={cn(thumbClassName, size === 'compact' && compactThumbClassName, thumbClasses)}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch, SwitchIndicator }
|
||||
Reference in New Issue
Block a user