mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 12:09:15 +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/
|
docs/superpowers/
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Design system (local-only; authoritative reference for agents + user)
|
||||||
|
frontend/DESIGN.md
|
||||||
|
.design-bundle/
|
||||||
|
|
||||||
# Playwright MCP
|
# Playwright MCP
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
|
||||||
|
|||||||
Generated
-1
@@ -24,7 +24,6 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { apiFetch } from '@/lib/api';
|
|||||||
import { toast } from '@/components/ui/toast-store';
|
import { toast } from '@/components/ui/toast-store';
|
||||||
import { Container, Network, Loader2, RefreshCw } from 'lucide-react';
|
import { Container, Network, Loader2, RefreshCw } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { Label } from '@/components/ui/label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -306,7 +306,7 @@ export default function NetworkTopologyView({ onContainerClick }: NetworkTopolog
|
|||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-card-border bg-card shadow-card-bevel overflow-hidden">
|
<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">
|
<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">
|
<Label htmlFor="show-system" className="text-xs cursor-pointer">
|
||||||
Show system networks
|
Show system networks
|
||||||
</Label>
|
</Label>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { toast } from '@/components/ui/toast-store';
|
import { toast } from '@/components/ui/toast-store';
|
||||||
@@ -1311,18 +1311,18 @@ export default function ResourcesView() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6 pt-1">
|
<div className="flex items-center gap-6 pt-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<TogglePill
|
||||||
id="net-internal"
|
id="net-internal"
|
||||||
checked={createNetworkForm.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>
|
<Label htmlFor="net-internal" className="text-xs cursor-pointer">Internal <span className="text-muted-foreground">(no external access)</span></Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<TogglePill
|
||||||
id="net-attachable"
|
id="net-attachable"
|
||||||
checked={createNetworkForm.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>
|
<Label htmlFor="net-attachable" className="text-xs cursor-pointer">Attachable</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Input } from '@/components/ui/input';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
@@ -142,9 +142,9 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={!!config.enabled}
|
checked={!!config.enabled}
|
||||||
onCheckedChange={(checked) => update('enabled', checked)}
|
onChange={(checked) => update('enabled', checked)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,9 +221,9 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={config.ldapTlsRejectUnauthorized !== false}
|
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>
|
<Label className="text-xs text-muted-foreground">Verify TLS certificate</Label>
|
||||||
</div>
|
</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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
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 { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Clock, Plus, Pencil, Trash2, History, RefreshCw, Play, ChevronLeft, ChevronRight, Download, CalendarClock, Table2 } from 'lucide-react';
|
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)}
|
{formatTimestamp(task.next_run_at)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={task.enabled === 1}
|
checked={task.enabled === 1}
|
||||||
onCheckedChange={() => handleToggle(task)}
|
onChange={() => handleToggle(task)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
@@ -742,7 +742,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<Label htmlFor="task-enabled">Enabled</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { Combobox } from '@/components/ui/combobox';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Trash2, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
|
import { Trash2, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
|
||||||
@@ -131,9 +131,9 @@ function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowPr
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={policy.enabled === 1}
|
checked={policy.enabled === 1}
|
||||||
onCheckedChange={(checked) => policy.id != null && onToggle(policy.id, checked)}
|
onChange={(checked) => policy.id != null && onToggle(policy.id, checked)}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
aria-label={`Toggle policy for ${policy.service_name ?? 'all services'}`}
|
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 { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { Separator } from '@/components/ui/separator';
|
||||||
import { AlertTriangle, RefreshCw, Shield, ShieldCheck } from 'lucide-react';
|
import { AlertTriangle, RefreshCw, Shield, ShieldCheck } from 'lucide-react';
|
||||||
import { MfaEnrollDialog } from '@/components/mfa/MfaEnrollDialog';
|
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.
|
SSO logins skip the second factor by default.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={mfa.sso_enforce_mfa}
|
checked={mfa.sso_enforce_mfa}
|
||||||
onCheckedChange={handleBypassToggle}
|
onChange={handleBypassToggle}
|
||||||
disabled={togglingBypass}
|
disabled={togglingBypass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
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>
|
<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>
|
<p className="text-xs text-muted-foreground">Enable Real-Time Metrics, Debug Diagnostics & Extended Logs</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<TogglePill
|
||||||
id="developer_mode"
|
id="developer_mode"
|
||||||
checked={settings.developer_mode === '1'}
|
checked={settings.developer_mode === '1'}
|
||||||
onCheckedChange={(c) => onSettingChange('developer_mode', c ? '1' : '0')}
|
onChange={(c) => onSettingChange('developer_mode', c ? '1' : '0')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
@@ -322,7 +322,7 @@ export function NotificationRoutingSection() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Enabled</Label>
|
<Label>Enabled</Label>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<Switch checked={formEnabled} onCheckedChange={setFormEnabled} />
|
<TogglePill checked={formEnabled} onChange={setFormEnabled} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,9 +373,9 @@ export function NotificationRoutingSection() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={route.enabled}
|
checked={route.enabled}
|
||||||
onCheckedChange={() => handleToggleEnabled(route)}
|
onChange={() => handleToggleEnabled(route)}
|
||||||
className="scale-75"
|
className="scale-75"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { springs } from '@/lib/motion';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { toast } from '@/components/ui/toast-store';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { useNodes } from '@/context/NodeContext';
|
import { useNodes } from '@/context/NodeContext';
|
||||||
@@ -96,10 +96,10 @@ export function NotificationsSection() {
|
|||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor={`${type}-enabled`} className="font-medium">Enable {title}</Label>
|
<Label htmlFor={`${type}-enabled`} className="font-medium">Enable {title}</Label>
|
||||||
<Switch
|
<TogglePill
|
||||||
id={`${type}-enabled`}
|
id={`${type}-enabled`}
|
||||||
checked={agents[type].enabled}
|
checked={agents[type].enabled}
|
||||||
onCheckedChange={(c) => handleAgentChange(type, 'enabled', c)}
|
onChange={(c) => handleAgentChange(type, 'enabled', c)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import {
|
import {
|
||||||
@@ -358,9 +358,9 @@ export function SecuritySection({ isPaid }: { isPaid: boolean }) {
|
|||||||
Check daily and install newer Trivy releases automatically.
|
Check daily and install newer Trivy releases automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={trivy.autoUpdate}
|
checked={trivy.autoUpdate}
|
||||||
onCheckedChange={handleAutoUpdateToggle}
|
onChange={handleAutoUpdateToggle}
|
||||||
disabled={trivyBusy !== null}
|
disabled={trivyBusy !== null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -483,9 +483,9 @@ export function SecuritySection({ isPaid }: { isPaid: boolean }) {
|
|||||||
Emit a critical alert when this policy is violated after a deploy.
|
Emit a critical alert when this policy is violated after a deploy.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={form.block_on_deploy}
|
checked={form.block_on_deploy}
|
||||||
onCheckedChange={(c) => setForm({ ...form, block_on_deploy: c })}
|
onChange={(c) => setForm({ ...form, block_on_deploy: c })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between rounded-lg border border-glass-border px-3 py-2.5">
|
<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>
|
<Label className="text-sm">Enabled</Label>
|
||||||
<p className="text-xs text-muted-foreground">Disabled policies are skipped during evaluation.</p>
|
<p className="text-xs text-muted-foreground">Disabled policies are skipped during evaluation.</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<TogglePill
|
||||||
checked={form.enabled}
|
checked={form.enabled}
|
||||||
onCheckedChange={(c) => setForm({ ...form, enabled: c })}
|
onChange={(c) => setForm({ ...form, enabled: c })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { toast } from '@/components/ui/toast-store';
|
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>
|
<Badge variant="secondary" className="text-[10px] shrink-0">{wh.stack_name}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<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!)}>
|
<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" />
|
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
||||||
</Button>
|
</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