mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(ui): replace Switch with TogglePill per design audit (#687)
* chore: ignore local design-system references Adds frontend/DESIGN.md and .design-bundle/ to .gitignore so the design-system reference and the audit handoff bundle stay local to each contributor's machine. * feat(ui): replace Switch with TogglePill per design audit Adds a mono-uppercase ON/OFF pill that replaces the sliding Switch across every call site. The pill uses role="switch" plus aria-checked, tints success-green when on and neutral card-tone when off, and holds a stable 60px min-width so the label flip does not shift layout. It reinforces the tracked-mono uppercase pattern already used for kicker labels and table column headers, and echoes the UP/DN status language of the sidebar stack list. - Add TogglePill component (frontend/src/components/ui/toggle-pill.tsx) - Replace 17 Switch call sites across 10 files (settings sections, SSO providers, scheduled operations, network topology, resources, stack auto-heal) - Remove unused Switch wrapper and radix primitive - Drop the @radix-ui/react-switch dependency
This commit is contained in:
@@ -50,6 +50,10 @@ MANUAL_STEPS.md
|
||||
docs/superpowers/
|
||||
.worktrees/
|
||||
|
||||
# Design system (local-only; authoritative reference for agents + user)
|
||||
frontend/DESIGN.md
|
||||
.design-bundle/
|
||||
|
||||
# Playwright MCP
|
||||
.playwright-mcp/
|
||||
|
||||
|
||||
Generated
-1
@@ -24,7 +24,6 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
|
||||
@@ -18,7 +18,7 @@ import { apiFetch } from '@/lib/api';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { Container, Network, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -306,7 +306,7 @@ export default function NetworkTopologyView({ onContainerClick }: NetworkTopolog
|
||||
return (
|
||||
<div className="rounded-lg border border-card-border bg-card shadow-card-bevel overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-card-border">
|
||||
<Switch id="show-system" checked={includeSystem} onCheckedChange={setIncludeSystem} />
|
||||
<TogglePill id="show-system" checked={includeSystem} onChange={setIncludeSystem} />
|
||||
<Label htmlFor="show-system" className="text-xs cursor-pointer">
|
||||
Show system networks
|
||||
</Label>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { TogglePill } from "@/components/ui/toggle-pill";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
@@ -1311,18 +1311,18 @@ export default function ResourcesView() {
|
||||
</div>
|
||||
<div className="flex items-center gap-6 pt-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
<TogglePill
|
||||
id="net-internal"
|
||||
checked={createNetworkForm.internal}
|
||||
onCheckedChange={v => setCreateNetworkForm(f => ({ ...f, internal: v }))}
|
||||
onChange={v => setCreateNetworkForm(f => ({ ...f, internal: v }))}
|
||||
/>
|
||||
<Label htmlFor="net-internal" className="text-xs cursor-pointer">Internal <span className="text-muted-foreground">(no external access)</span></Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
<TogglePill
|
||||
id="net-attachable"
|
||||
checked={createNetworkForm.attachable}
|
||||
onCheckedChange={v => setCreateNetworkForm(f => ({ ...f, attachable: v }))}
|
||||
onChange={v => setCreateNetworkForm(f => ({ ...f, attachable: v }))}
|
||||
/>
|
||||
<Label htmlFor="net-attachable" className="text-xs cursor-pointer">Attachable</Label>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
@@ -142,9 +142,9 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={!!config.enabled}
|
||||
onCheckedChange={(checked) => update('enabled', checked)}
|
||||
onChange={(checked) => update('enabled', checked)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
@@ -221,9 +221,9 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={config.ldapTlsRejectUnauthorized !== false}
|
||||
onCheckedChange={checked => update('ldapTlsRejectUnauthorized', checked)}
|
||||
onChange={checked => update('ldapTlsRejectUnauthorized', checked)}
|
||||
/>
|
||||
<Label className="text-xs text-muted-foreground">Verify TLS certificate</Label>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Clock, Plus, Pencil, Trash2, History, RefreshCw, Play, ChevronLeft, ChevronRight, Download, CalendarClock, Table2 } from 'lucide-react';
|
||||
@@ -588,9 +588,9 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
||||
{formatTimestamp(task.next_run_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={task.enabled === 1}
|
||||
onCheckedChange={() => handleToggle(task)}
|
||||
onChange={() => handleToggle(task)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
@@ -742,7 +742,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={formEnabled} onCheckedChange={setFormEnabled} id="task-enabled" />
|
||||
<TogglePill checked={formEnabled} onChange={setFormEnabled} id="task-enabled" />
|
||||
<Label htmlFor="task-enabled">Enabled</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Trash2, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
|
||||
@@ -131,9 +131,9 @@ function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowPr
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={policy.enabled === 1}
|
||||
onCheckedChange={(checked) => policy.id != null && onToggle(policy.id, checked)}
|
||||
onChange={(checked) => policy.id != null && onToggle(policy.id, checked)}
|
||||
disabled={saving}
|
||||
aria-label={`Toggle policy for ${policy.service_name ?? 'all services'}`}
|
||||
/>
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/* eslint-disable */
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Switch as SwitchPrimitives } from 'radix-ui';
|
||||
import {
|
||||
motion,
|
||||
type TargetAndTransition,
|
||||
type VariantLabels,
|
||||
type HTMLMotionProps,
|
||||
type LegacyAnimationControls,
|
||||
} from 'motion/react';
|
||||
|
||||
import { getStrictContext } from '@/lib/get-strict-context';
|
||||
import { useControlledState } from '@/hooks/use-controlled-state';
|
||||
|
||||
type SwitchContextType = {
|
||||
isChecked: boolean;
|
||||
setIsChecked: (isChecked: boolean) => void;
|
||||
isPressed: boolean;
|
||||
setIsPressed: (isPressed: boolean) => void;
|
||||
};
|
||||
|
||||
const [SwitchProvider, useSwitch] =
|
||||
getStrictContext<SwitchContextType>('SwitchContext');
|
||||
|
||||
type SwitchProps = Omit<
|
||||
React.ComponentProps<typeof SwitchPrimitives.Root>,
|
||||
'asChild'
|
||||
> &
|
||||
HTMLMotionProps<'button'>;
|
||||
|
||||
function Switch(props: SwitchProps) {
|
||||
// Destructure Radix-only props so they don't leak onto the motion.button DOM element
|
||||
const {
|
||||
checked,
|
||||
defaultChecked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
required,
|
||||
name,
|
||||
value,
|
||||
form,
|
||||
...motionProps
|
||||
} = props;
|
||||
|
||||
const [isPressed, setIsPressed] = React.useState(false);
|
||||
const [isChecked, setIsChecked] = useControlledState({
|
||||
value: checked,
|
||||
defaultValue: defaultChecked,
|
||||
onChange: onCheckedChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<SwitchProvider
|
||||
value={{ isChecked, setIsChecked, isPressed, setIsPressed }}
|
||||
>
|
||||
<SwitchPrimitives.Root
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
onCheckedChange={setIsChecked}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
name={name}
|
||||
value={value}
|
||||
form={form}
|
||||
asChild
|
||||
>
|
||||
<motion.button
|
||||
data-slot="switch"
|
||||
whileTap="tap"
|
||||
initial={false}
|
||||
onTapStart={() => setIsPressed(true)}
|
||||
onTapCancel={() => setIsPressed(false)}
|
||||
onTap={() => setIsPressed(false)}
|
||||
{...motionProps}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
</SwitchProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type SwitchThumbProps = Omit<
|
||||
React.ComponentProps<typeof SwitchPrimitives.Thumb>,
|
||||
'asChild'
|
||||
> &
|
||||
HTMLMotionProps<'div'> & {
|
||||
pressedAnimation?:
|
||||
| TargetAndTransition
|
||||
| VariantLabels
|
||||
| boolean
|
||||
| LegacyAnimationControls;
|
||||
};
|
||||
|
||||
function SwitchThumb({
|
||||
pressedAnimation,
|
||||
transition = { type: 'spring', stiffness: 300, damping: 25 },
|
||||
...props
|
||||
}: SwitchThumbProps) {
|
||||
const { isPressed } = useSwitch();
|
||||
|
||||
return (
|
||||
<SwitchPrimitives.Thumb asChild>
|
||||
<motion.div
|
||||
data-slot="switch-thumb"
|
||||
whileTap="tab"
|
||||
layout
|
||||
transition={transition}
|
||||
animate={isPressed ? pressedAnimation : undefined}
|
||||
{...props}
|
||||
/>
|
||||
</SwitchPrimitives.Thumb>
|
||||
);
|
||||
}
|
||||
|
||||
type SwitchIconPosition = 'left' | 'right' | 'thumb';
|
||||
|
||||
type SwitchIconProps = HTMLMotionProps<'div'> & {
|
||||
position: SwitchIconPosition;
|
||||
};
|
||||
|
||||
function SwitchIcon({
|
||||
position,
|
||||
transition = { type: 'spring', bounce: 0 },
|
||||
...props
|
||||
}: SwitchIconProps) {
|
||||
const { isChecked } = useSwitch();
|
||||
|
||||
const isAnimated = React.useMemo(() => {
|
||||
if (position === 'right') return !isChecked;
|
||||
if (position === 'left') return isChecked;
|
||||
if (position === 'thumb') return true;
|
||||
return false;
|
||||
}, [position, isChecked]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
data-slot={`switch-${position}-icon`}
|
||||
animate={isAnimated ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 }}
|
||||
transition={transition}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Switch,
|
||||
SwitchThumb,
|
||||
SwitchIcon,
|
||||
useSwitch,
|
||||
type SwitchProps,
|
||||
type SwitchThumbProps,
|
||||
type SwitchIconProps,
|
||||
type SwitchIconPosition,
|
||||
type SwitchContextType,
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { AlertTriangle, RefreshCw, Shield, ShieldCheck } from 'lucide-react';
|
||||
import { MfaEnrollDialog } from '@/components/mfa/MfaEnrollDialog';
|
||||
@@ -179,9 +179,9 @@ export function AccountSection({ authData, onAuthDataChange, onPasswordChange, i
|
||||
SSO logins skip the second factor by default.
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={mfa.sso_enforce_mfa}
|
||||
onCheckedChange={handleBypassToggle}
|
||||
onChange={handleBypassToggle}
|
||||
disabled={togglingBypass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
@@ -64,10 +64,10 @@ export function DeveloperSection({ settings, onSettingChange, onSave, isSaving,
|
||||
<Label htmlFor="developer_mode" className="text-base">Developer Mode</Label>
|
||||
<p className="text-xs text-muted-foreground">Enable Real-Time Metrics, Debug Diagnostics & Extended Logs</p>
|
||||
</div>
|
||||
<Switch
|
||||
<TogglePill
|
||||
id="developer_mode"
|
||||
checked={settings.developer_mode === '1'}
|
||||
onCheckedChange={(c) => onSettingChange('developer_mode', c ? '1' : '0')}
|
||||
onChange={(c) => onSettingChange('developer_mode', c ? '1' : '0')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
@@ -322,7 +322,7 @@ export function NotificationRoutingSection() {
|
||||
<div className="space-y-2">
|
||||
<Label>Enabled</Label>
|
||||
<div className="pt-2">
|
||||
<Switch checked={formEnabled} onCheckedChange={setFormEnabled} />
|
||||
<TogglePill checked={formEnabled} onChange={setFormEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -373,9 +373,9 @@ export function NotificationRoutingSection() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={route.enabled}
|
||||
onCheckedChange={() => handleToggleEnabled(route)}
|
||||
onChange={() => handleToggleEnabled(route)}
|
||||
className="scale-75"
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -4,7 +4,7 @@ import { springs } from '@/lib/motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { useNodes } from '@/context/NodeContext';
|
||||
@@ -96,10 +96,10 @@ export function NotificationsSection() {
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor={`${type}-enabled`} className="font-medium">Enable {title}</Label>
|
||||
<Switch
|
||||
<TogglePill
|
||||
id={`${type}-enabled`}
|
||||
checked={agents[type].enabled}
|
||||
onCheckedChange={(c) => handleAgentChange(type, 'enabled', c)}
|
||||
onChange={(c) => handleAgentChange(type, 'enabled', c)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import {
|
||||
@@ -358,9 +358,9 @@ export function SecuritySection({ isPaid }: { isPaid: boolean }) {
|
||||
Check daily and install newer Trivy releases automatically.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={trivy.autoUpdate}
|
||||
onCheckedChange={handleAutoUpdateToggle}
|
||||
onChange={handleAutoUpdateToggle}
|
||||
disabled={trivyBusy !== null}
|
||||
/>
|
||||
</div>
|
||||
@@ -483,9 +483,9 @@ export function SecuritySection({ isPaid }: { isPaid: boolean }) {
|
||||
Emit a critical alert when this policy is violated after a deploy.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={form.block_on_deploy}
|
||||
onCheckedChange={(c) => setForm({ ...form, block_on_deploy: c })}
|
||||
onChange={(c) => setForm({ ...form, block_on_deploy: c })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border border-glass-border px-3 py-2.5">
|
||||
@@ -493,9 +493,9 @@ export function SecuritySection({ isPaid }: { isPaid: boolean }) {
|
||||
<Label className="text-sm">Enabled</Label>
|
||||
<p className="text-xs text-muted-foreground">Disabled policies are skipped during evaluation.</p>
|
||||
</div>
|
||||
<Switch
|
||||
<TogglePill
|
||||
checked={form.enabled}
|
||||
onCheckedChange={(c) => setForm({ ...form, enabled: c })}
|
||||
onChange={(c) => setForm({ ...form, enabled: c })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
@@ -244,7 +244,7 @@ export function WebhooksSection({ isPaid }: { isPaid: boolean }) {
|
||||
<Badge variant="secondary" className="text-[10px] shrink-0">{wh.stack_name}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Switch checked={wh.enabled} onCheckedChange={(c) => handleToggle(wh.id!, c)} />
|
||||
<TogglePill checked={wh.enabled} onChange={(c) => handleToggle(wh.id!, c)} />
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => handleDelete(wh.id!)}>
|
||||
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
Switch as AnimateSwitch,
|
||||
SwitchThumb,
|
||||
type SwitchProps as AnimateSwitchProps,
|
||||
} from '@/components/animate-ui/primitives/radix/switch';
|
||||
|
||||
type SwitchProps = Omit<AnimateSwitchProps, 'children'>;
|
||||
|
||||
const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<AnimateSwitch
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchThumb
|
||||
className="pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
pressedAnimation={{ scale: 1.15 }}
|
||||
/>
|
||||
</AnimateSwitch>
|
||||
)
|
||||
);
|
||||
Switch.displayName = 'Switch';
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TogglePillProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
||||
checked: boolean;
|
||||
onChange: (next: boolean) => void;
|
||||
}
|
||||
|
||||
const TogglePill = React.forwardRef<HTMLButtonElement, TogglePillProps>(
|
||||
({ checked, onChange, className, disabled, onClick, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
if (event.defaultPrevented) return;
|
||||
onChange(!checked);
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md border px-2.5 py-1 font-mono text-xs uppercase tracking-[0.18em] transition-colors min-w-[60px] focus-visible:ring-2 focus-visible:ring-brand/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
checked
|
||||
? 'border-success/30 bg-success/10 text-success hover:bg-success/15'
|
||||
: 'border-card-border bg-card text-stat-subtitle hover:text-stat-value',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{checked ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
TogglePill.displayName = 'TogglePill';
|
||||
|
||||
export { TogglePill };
|
||||
export type { TogglePillProps };
|
||||
Reference in New Issue
Block a user