feat: unify boolean controls as buttons

This commit is contained in:
JetSprow
2026-04-30 16:04:59 +10:00
parent 2591402f70
commit 901219f39c
7 changed files with 209 additions and 156 deletions

View File

@@ -11,6 +11,7 @@ import {
updateAnnouncement, updateAnnouncement,
} from "@/actions/admin/announcements"; } from "@/actions/admin/announcements";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button"; import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -136,15 +137,14 @@ export function AnnouncementForm({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`dismissible-${announcement.id}`}></Label> <Label htmlFor={`dismissible-${announcement.id}`}></Label>
<select <BooleanToggle
id={`dismissible-${announcement.id}`} id={`dismissible-${announcement.id}`}
name="dismissible" name="dismissible"
defaultValue={announcement.dismissible ? "true" : "false"} defaultValue={announcement.dismissible}
className="h-10 w-full px-3 text-sm outline-none" trueLabel="允许"
> falseLabel="不允许"
<option value="true"></option> ariaLabel="允许用户关闭"
<option value="false"></option> />
</select>
</div> </div>
</div> </div>
@@ -200,15 +200,14 @@ export function AnnouncementForm({
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`sendNotification-${announcement.id}`}></Label> <Label htmlFor={`sendNotification-${announcement.id}`}></Label>
<select <BooleanToggle
id={`sendNotification-${announcement.id}`} id={`sendNotification-${announcement.id}`}
name="sendNotification" name="sendNotification"
defaultValue={announcement.sendNotification ? "true" : "false"} defaultValue={announcement.sendNotification}
className="h-10 w-full px-3 text-sm outline-none" trueLabel="发送"
> falseLabel="不发送"
<option value="true"></option> ariaLabel="同步发送站内通知"
<option value="false"></option> />
</select>
</div> </div>
<PendingSubmitButton className="w-full" pendingLabel="保存中..."> <PendingSubmitButton className="w-full" pendingLabel="保存中...">
@@ -285,15 +284,14 @@ export function CreateAnnouncementButton({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="create-announcement-dismissible"></Label> <Label htmlFor="create-announcement-dismissible"></Label>
<select <BooleanToggle
id="create-announcement-dismissible" id="create-announcement-dismissible"
name="dismissible" name="dismissible"
defaultValue="true" defaultValue
className="h-10 w-full px-3 text-sm outline-none" trueLabel="允许"
> falseLabel="不允许"
<option value="true"></option> ariaLabel="允许用户关闭"
<option value="false"></option> />
</select>
</div> </div>
</div> </div>
@@ -333,15 +331,14 @@ export function CreateAnnouncementButton({
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="create-announcement-sendNotification"></Label> <Label htmlFor="create-announcement-sendNotification"></Label>
<select <BooleanToggle
id="create-announcement-sendNotification" id="create-announcement-sendNotification"
name="sendNotification" name="sendNotification"
defaultValue="true" defaultValue
className="h-10 w-full px-3 text-sm outline-none" trueLabel="发送"
> falseLabel="不发送"
<option value="true"></option> ariaLabel="同步发送站内通知"
<option value="false"></option> />
</select>
</div> </div>
<PendingSubmitButton className="w-full" pendingLabel="发布中..."> <PendingSubmitButton className="w-full" pendingLabel="发布中...">

View File

@@ -5,6 +5,7 @@ import { DetailItem, DetailList } from "@/components/admin/detail-list";
import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge"; import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge";
import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell"; import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell";
import { PendingSubmitButton } from "@/components/shared/pending-submit-button"; import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -68,10 +69,14 @@ export default async function AdminCommercePage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="coupon-public"></Label> <Label htmlFor="coupon-public"></Label>
<select id="coupon-public" name="isPublic" className={selectClassName} defaultValue="true"> <BooleanToggle
<option value="true"></option> id="coupon-public"
<option value="false"></option> name="isPublic"
</select> defaultValue
trueLabel="公开展示"
falseLabel="仅发放"
ariaLabel="用户可见"
/>
</div> </div>
<PendingSubmitButton className="w-full" pendingLabel="创建中..."></PendingSubmitButton> <PendingSubmitButton className="w-full" pendingLabel="创建中..."></PendingSubmitButton>
</form> </form>

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
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 { savePaymentConfig } from "@/actions/admin/payments"; import { savePaymentConfig } from "@/actions/admin/payments";
import { getErrorMessage } from "@/lib/errors"; import { getErrorMessage } from "@/lib/errors";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -121,9 +121,15 @@ export function PaymentConfigForm({
)} )}
</div> </div>
<div className="flex flex-col gap-3 border-t border-border/50 pt-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 border-t border-border/50 pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2"> <div className="w-full sm:w-56">
<Switch checked={enabled} onCheckedChange={setEnabled} /> <Label className="mb-2 block text-xs text-muted-foreground"></Label>
<span className="text-sm">{enabled ? "已启用" : "未启用"}</span> <BooleanToggle
value={enabled}
onChange={setEnabled}
trueLabel="启用"
falseLabel="停用"
ariaLabel="支付通道状态"
/>
</div> </div>
<Button type="submit" size="sm" disabled={saving}> <Button type="submit" size="sm" disabled={saving}>
{saving ? "保存中..." : "保存配置"} {saving ? "保存中..." : "保存配置"}

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
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 { import {
@@ -10,7 +11,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import type { PlanFormValue, PlanType } from "./plan-form-types"; import type { PlanFormValue, PlanType } from "./plan-form-types";
type FieldId = (name: string) => string; type FieldId = (name: string) => string;
@@ -52,7 +52,15 @@ export function PlanPolicySection({
<p id={fieldId("allowRenewal-label")} className="text-sm font-medium"></p> <p id={fieldId("allowRenewal-label")} className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"></p> <p className="text-xs text-muted-foreground"></p>
</div> </div>
<Switch aria-labelledby={fieldId("allowRenewal-label")} checked={allowRenewal} onCheckedChange={setAllowRenewal} /> <div className="w-40">
<BooleanToggle
value={allowRenewal}
onChange={setAllowRenewal}
trueLabel="开放"
falseLabel="关闭"
ariaLabel="开放续费"
/>
</div>
</div> </div>
{type === "PROXY" && ( {type === "PROXY" && (
<div className="flex items-center justify-between gap-4 rounded-lg bg-muted/20 p-3"> <div className="flex items-center justify-between gap-4 rounded-lg bg-muted/20 p-3">
@@ -60,7 +68,15 @@ export function PlanPolicySection({
<p id={fieldId("allowTrafficTopup-label")} className="text-sm font-medium"></p> <p id={fieldId("allowTrafficTopup-label")} className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground"> GB</p> <p className="text-xs text-muted-foreground"> GB</p>
</div> </div>
<Switch aria-labelledby={fieldId("allowTrafficTopup-label")} checked={allowTrafficTopup} onCheckedChange={setAllowTrafficTopup} /> <div className="w-40">
<BooleanToggle
value={allowTrafficTopup}
onChange={setAllowTrafficTopup}
trueLabel="开放"
falseLabel="关闭"
ariaLabel="开放增流量"
/>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -3,6 +3,7 @@
import { useState, type FormEvent } from "react"; import { useState, type FormEvent } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Bell, ChevronDown, Clock3, Gift, LifeBuoy, Mail, RadioTower, Send, Settings2, ShieldAlert, ShieldCheck } from "lucide-react"; import { Bell, ChevronDown, Clock3, Gift, LifeBuoy, Mail, RadioTower, Send, Settings2, ShieldAlert, ShieldCheck } from "lucide-react";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } 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";
@@ -205,15 +206,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-2"> <div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="autoReminderDispatchEnabled"></Label> <Label htmlFor="autoReminderDispatchEnabled"></Label>
<select <BooleanToggle
id="autoReminderDispatchEnabled" id="autoReminderDispatchEnabled"
name="autoReminderDispatchEnabled" name="autoReminderDispatchEnabled"
defaultValue={String(config.autoReminderDispatchEnabled)} defaultValue={config.autoReminderDispatchEnabled}
className={selectClassName} ariaLabel="自动提醒派发"
> />
<option value="true"></option>
<option value="false"></option>
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="reminderDispatchIntervalMinutes"></Label> <Label htmlFor="reminderDispatchIntervalMinutes"></Label>
@@ -221,15 +219,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="trafficSyncEnabled">3x-ui </Label> <Label htmlFor="trafficSyncEnabled">3x-ui </Label>
<select <BooleanToggle
id="trafficSyncEnabled" id="trafficSyncEnabled"
name="trafficSyncEnabled" name="trafficSyncEnabled"
defaultValue={String(config.trafficSyncEnabled)} defaultValue={config.trafficSyncEnabled}
className={selectClassName} ariaLabel="3x-ui 流量定时同步"
> />
<option value="true"></option>
<option value="false"></option>
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="trafficSyncIntervalSeconds"></Label> <Label htmlFor="trafficSyncIntervalSeconds"></Label>
@@ -254,30 +249,24 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-2"> <div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="networkRecommendationsEnabled"></Label> <Label htmlFor="networkRecommendationsEnabled"></Label>
<select <BooleanToggle
id="networkRecommendationsEnabled" id="networkRecommendationsEnabled"
name="networkRecommendationsEnabled" name="networkRecommendationsEnabled"
defaultValue={String(config.networkRecommendationsEnabled)} defaultValue={config.networkRecommendationsEnabled}
className={selectClassName} ariaLabel="三网推荐"
> />
<option value="false"></option>
<option value="true"></option>
</select>
<p className="text-xs leading-5 text-muted-foreground"> <p className="text-xs leading-5 text-muted-foreground">
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="networkInsightsEnabled">线</Label> <Label htmlFor="networkInsightsEnabled">线</Label>
<select <BooleanToggle
id="networkInsightsEnabled" id="networkInsightsEnabled"
name="networkInsightsEnabled" name="networkInsightsEnabled"
defaultValue={String(config.networkInsightsEnabled)} defaultValue={config.networkInsightsEnabled}
className={selectClassName} ariaLabel="线路体验"
> />
<option value="false"></option>
<option value="true"></option>
</select>
<p className="text-xs leading-5 text-muted-foreground"> <p className="text-xs leading-5 text-muted-foreground">
访线 访线
</p> </p>
@@ -313,27 +302,23 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-3"> <div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="subscriptionRiskEnabled"></Label> <Label htmlFor="subscriptionRiskEnabled"></Label>
<select <BooleanToggle
id="subscriptionRiskEnabled" id="subscriptionRiskEnabled"
name="subscriptionRiskEnabled" name="subscriptionRiskEnabled"
defaultValue={String(config.subscriptionRiskEnabled)} defaultValue={config.subscriptionRiskEnabled}
className={selectClassName} ariaLabel="风控总控"
> />
<option value="true"></option>
<option value="false"></option>
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="subscriptionRiskAutoSuspend"></Label> <Label htmlFor="subscriptionRiskAutoSuspend"></Label>
<select <BooleanToggle
id="subscriptionRiskAutoSuspend" id="subscriptionRiskAutoSuspend"
name="subscriptionRiskAutoSuspend" name="subscriptionRiskAutoSuspend"
defaultValue={String(config.subscriptionRiskAutoSuspend)} defaultValue={config.subscriptionRiskAutoSuspend}
className={selectClassName} trueLabel="开启自动封停"
> falseLabel="只记录警告"
<option value="true"></option> ariaLabel="自动暂停"
<option value="false"></option> />
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="subscriptionRiskWindowHours"></Label> <Label htmlFor="subscriptionRiskWindowHours"></Label>
@@ -436,15 +421,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nodeAccessRiskEnabled"></Label> <Label htmlFor="nodeAccessRiskEnabled"></Label>
<select <BooleanToggle
id="nodeAccessRiskEnabled" id="nodeAccessRiskEnabled"
name="nodeAccessRiskEnabled" name="nodeAccessRiskEnabled"
defaultValue={String(config.nodeAccessRiskEnabled)} defaultValue={config.nodeAccessRiskEnabled}
className={selectClassName} trueLabel="接收日志"
> falseLabel="仅订阅风控"
<option value="true"> Agent Xray </option> ariaLabel="节点日志风控"
<option value="false"></option> />
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nodeAccessConnectionWarning"></Label> <Label htmlFor="nodeAccessConnectionWarning"></Label>
@@ -477,39 +461,36 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-2"> <div className="grid gap-5 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="allowRegistration"></Label> <Label htmlFor="allowRegistration"></Label>
<select <BooleanToggle
id="allowRegistration" id="allowRegistration"
name="allowRegistration" name="allowRegistration"
defaultValue={String(config.allowRegistration)} defaultValue={config.allowRegistration}
className={selectClassName} trueLabel="开放"
> falseLabel="关闭"
<option value="true"></option> ariaLabel="开放注册"
<option value="false"></option> />
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="requireInviteCode"></Label> <Label htmlFor="requireInviteCode"></Label>
<select <BooleanToggle
id="requireInviteCode" id="requireInviteCode"
name="requireInviteCode" name="requireInviteCode"
defaultValue={String(config.requireInviteCode)} defaultValue={config.requireInviteCode}
className={selectClassName} trueLabel="必须"
> falseLabel="不需要"
<option value="false"></option> ariaLabel="注册必须邀请码"
<option value="true"></option> />
</select>
</div> </div>
<div className="space-y-2 md:col-span-2"> <div className="space-y-2 md:col-span-2">
<Label htmlFor="emailVerificationRequired"></Label> <Label htmlFor="emailVerificationRequired"></Label>
<select <BooleanToggle
id="emailVerificationRequired" id="emailVerificationRequired"
name="emailVerificationRequired" name="emailVerificationRequired"
defaultValue={String(config.emailVerificationRequired)} defaultValue={config.emailVerificationRequired}
className={selectClassName} trueLabel="开启验证"
> falseLabel="关闭"
<option value="false"></option> ariaLabel="注册邮箱验证"
<option value="true"></option> />
</select>
<p className="text-xs leading-5 text-muted-foreground"></p> <p className="text-xs leading-5 text-muted-foreground"></p>
</div> </div>
</div> </div>
@@ -525,10 +506,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-3"> <div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="smtpEnabled"></Label> <Label htmlFor="smtpEnabled"></Label>
<select id="smtpEnabled" name="smtpEnabled" defaultValue={String(config.smtpEnabled)} className={selectClassName}> <BooleanToggle
<option value="false"></option> id="smtpEnabled"
<option value="true"></option> name="smtpEnabled"
</select> defaultValue={config.smtpEnabled}
ariaLabel="邮件服务"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="smtpHost">SMTP </Label> <Label htmlFor="smtpHost">SMTP </Label>
@@ -540,10 +523,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="smtpSecure">TLS / SSL</Label> <Label htmlFor="smtpSecure">TLS / SSL</Label>
<select id="smtpSecure" name="smtpSecure" defaultValue={String(config.smtpSecure)} className={selectClassName}> <BooleanToggle
<option value="false">STARTTLS / </option> id="smtpSecure"
<option value="true">SSL </option> name="smtpSecure"
</select> defaultValue={config.smtpSecure}
trueLabel="SSL 直连"
falseLabel="STARTTLS"
ariaLabel="TLS / SSL"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="smtpUser">SMTP </Label> <Label htmlFor="smtpUser">SMTP </Label>
@@ -581,15 +568,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<div className="grid gap-5 md:grid-cols-3"> <div className="grid gap-5 md:grid-cols-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="inviteRewardEnabled"></Label> <Label htmlFor="inviteRewardEnabled"></Label>
<select <BooleanToggle
id="inviteRewardEnabled" id="inviteRewardEnabled"
name="inviteRewardEnabled" name="inviteRewardEnabled"
defaultValue={String(config.inviteRewardEnabled)} defaultValue={config.inviteRewardEnabled}
className={selectClassName} ariaLabel="自动发放奖励"
> />
<option value="false"></option>
<option value="true"></option>
</select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="inviteRewardRate">%</Label> <Label htmlFor="inviteRewardRate">%</Label>

View File

@@ -0,0 +1,77 @@
"use client";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";
interface BooleanToggleProps {
id?: string;
name?: string;
value?: boolean;
defaultValue?: boolean;
onChange?: (value: boolean) => void;
trueLabel?: string;
falseLabel?: string;
ariaLabel?: string;
className?: string;
disabled?: boolean;
}
export function BooleanToggle({
id,
name,
value,
defaultValue = false,
onChange,
trueLabel = "开启",
falseLabel = "关闭",
ariaLabel,
className,
disabled = false,
}: BooleanToggleProps) {
const generatedId = useId();
const inputId = id ?? generatedId;
const controlled = value != null;
const [internalValue, setInternalValue] = useState(defaultValue);
const currentValue = controlled ? value : internalValue;
function select(nextValue: boolean) {
if (disabled) return;
if (!controlled) setInternalValue(nextValue);
onChange?.(nextValue);
}
return (
<div className={cn("w-full", className)}>
{name && <input id={inputId} type="hidden" name={name} value={String(currentValue)} />}
<div
role="group"
aria-label={ariaLabel}
className="inline-flex min-h-10 w-full rounded-lg border border-border bg-muted/25 p-1"
>
{[
{ value: true, label: trueLabel },
{ value: false, label: falseLabel },
].map((option) => {
const active = currentValue === option.value;
return (
<button
key={String(option.value)}
type="button"
aria-pressed={active}
disabled={disabled}
onClick={() => select(option.value)}
className={cn(
"min-w-0 flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-60",
active
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:bg-background/55 hover:text-foreground",
)}
>
<span className="truncate">{option.label}</span>
</button>
);
})}
</div>
</div>
);
}

View File

@@ -1,32 +0,0 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }