mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: unify boolean controls as buttons
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
updateAnnouncement,
|
||||
} from "@/actions/admin/announcements";
|
||||
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
|
||||
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -136,15 +137,14 @@ export function AnnouncementForm({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`dismissible-${announcement.id}`}>允许用户关闭</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id={`dismissible-${announcement.id}`}
|
||||
name="dismissible"
|
||||
defaultValue={announcement.dismissible ? "true" : "false"}
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
defaultValue={announcement.dismissible}
|
||||
trueLabel="允许"
|
||||
falseLabel="不允许"
|
||||
ariaLabel="允许用户关闭"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -200,15 +200,14 @@ export function AnnouncementForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`sendNotification-${announcement.id}`}>同步发送站内通知</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id={`sendNotification-${announcement.id}`}
|
||||
name="sendNotification"
|
||||
defaultValue={announcement.sendNotification ? "true" : "false"}
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
defaultValue={announcement.sendNotification}
|
||||
trueLabel="发送"
|
||||
falseLabel="不发送"
|
||||
ariaLabel="同步发送站内通知"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PendingSubmitButton className="w-full" pendingLabel="保存中...">
|
||||
@@ -285,15 +284,14 @@ export function CreateAnnouncementButton({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-announcement-dismissible">允许用户关闭</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="create-announcement-dismissible"
|
||||
name="dismissible"
|
||||
defaultValue="true"
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
defaultValue
|
||||
trueLabel="允许"
|
||||
falseLabel="不允许"
|
||||
ariaLabel="允许用户关闭"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -333,15 +331,14 @@ export function CreateAnnouncementButton({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-announcement-sendNotification">同步发送站内通知</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="create-announcement-sendNotification"
|
||||
name="sendNotification"
|
||||
defaultValue="true"
|
||||
className="h-10 w-full px-3 text-sm outline-none"
|
||||
>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
defaultValue
|
||||
trueLabel="发送"
|
||||
falseLabel="不发送"
|
||||
ariaLabel="同步发送站内通知"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PendingSubmitButton className="w-full" pendingLabel="发布中...">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DetailItem, DetailList } from "@/components/admin/detail-list";
|
||||
import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge";
|
||||
import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell";
|
||||
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
|
||||
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
@@ -68,10 +69,14 @@ export default async function AdminCommercePage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="coupon-public">用户可见</Label>
|
||||
<select id="coupon-public" name="isPublic" className={selectClassName} defaultValue="true">
|
||||
<option value="true">公开展示</option>
|
||||
<option value="false">仅发放可用</option>
|
||||
</select>
|
||||
<BooleanToggle
|
||||
id="coupon-public"
|
||||
name="isPublic"
|
||||
defaultValue
|
||||
trueLabel="公开展示"
|
||||
falseLabel="仅发放"
|
||||
ariaLabel="用户可见"
|
||||
/>
|
||||
</div>
|
||||
<PendingSubmitButton className="w-full" pendingLabel="创建中...">创建优惠券</PendingSubmitButton>
|
||||
</form>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||
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 { savePaymentConfig } from "@/actions/admin/payments";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
import { toast } from "sonner";
|
||||
@@ -121,9 +121,15 @@ export function PaymentConfigForm({
|
||||
)}
|
||||
</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 items-center gap-2">
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
<span className="text-sm">{enabled ? "已启用" : "未启用"}</span>
|
||||
<div className="w-full sm:w-56">
|
||||
<Label className="mb-2 block text-xs text-muted-foreground">支付通道状态</Label>
|
||||
<BooleanToggle
|
||||
value={enabled}
|
||||
onChange={setEnabled}
|
||||
trueLabel="启用"
|
||||
falseLabel="停用"
|
||||
ariaLabel="支付通道状态"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" size="sm" disabled={saving}>
|
||||
{saving ? "保存中..." : "保存配置"}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { PlanFormValue, PlanType } from "./plan-form-types";
|
||||
|
||||
type FieldId = (name: string) => string;
|
||||
@@ -52,7 +52,15 @@ export function PlanPolicySection({
|
||||
<p id={fieldId("allowRenewal-label")} className="text-sm font-medium">开放续费</p>
|
||||
<p className="text-xs text-muted-foreground">用户可拖动选择续费时长</p>
|
||||
</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>
|
||||
{type === "PROXY" && (
|
||||
<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 className="text-xs text-muted-foreground">用户可拖动选择加多少 GB</p>
|
||||
</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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 { Input } from "@/components/ui/input";
|
||||
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="space-y-2">
|
||||
<Label htmlFor="autoReminderDispatchEnabled">自动提醒派发</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="autoReminderDispatchEnabled"
|
||||
name="autoReminderDispatchEnabled"
|
||||
defaultValue={String(config.autoReminderDispatchEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="true">开启</option>
|
||||
<option value="false">关闭</option>
|
||||
</select>
|
||||
defaultValue={config.autoReminderDispatchEnabled}
|
||||
ariaLabel="自动提醒派发"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reminderDispatchIntervalMinutes">提醒间隔(分钟)</Label>
|
||||
@@ -221,15 +219,12 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="trafficSyncEnabled">3x-ui 流量定时同步</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="trafficSyncEnabled"
|
||||
name="trafficSyncEnabled"
|
||||
defaultValue={String(config.trafficSyncEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="true">开启</option>
|
||||
<option value="false">关闭</option>
|
||||
</select>
|
||||
defaultValue={config.trafficSyncEnabled}
|
||||
ariaLabel="3x-ui 流量定时同步"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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="space-y-2">
|
||||
<Label htmlFor="networkRecommendationsEnabled">三网推荐</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="networkRecommendationsEnabled"
|
||||
name="networkRecommendationsEnabled"
|
||||
defaultValue={String(config.networkRecommendationsEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="false">关闭</option>
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
defaultValue={config.networkRecommendationsEnabled}
|
||||
ariaLabel="三网推荐"
|
||||
/>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
开启后,商城展示电信、联通、移动当前最低延迟推荐;点击推荐会直接打开对应套餐详情。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="networkInsightsEnabled">线路体验</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="networkInsightsEnabled"
|
||||
name="networkInsightsEnabled"
|
||||
defaultValue={String(config.networkInsightsEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="false">关闭</option>
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
defaultValue={config.networkInsightsEnabled}
|
||||
ariaLabel="线路体验"
|
||||
/>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
开启后,套餐详情展示节点延迟、趋势和访问路径;关闭后只保留购买所需的线路入口选择。
|
||||
</p>
|
||||
@@ -313,27 +302,23 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
<div className="grid gap-5 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subscriptionRiskEnabled">风控总控</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="subscriptionRiskEnabled"
|
||||
name="subscriptionRiskEnabled"
|
||||
defaultValue={String(config.subscriptionRiskEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="true">开启</option>
|
||||
<option value="false">关闭</option>
|
||||
</select>
|
||||
defaultValue={config.subscriptionRiskEnabled}
|
||||
ariaLabel="风控总控"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subscriptionRiskAutoSuspend">自动暂停</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="subscriptionRiskAutoSuspend"
|
||||
name="subscriptionRiskAutoSuspend"
|
||||
defaultValue={String(config.subscriptionRiskAutoSuspend)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="true">开启,达到暂停阈值自动封停</option>
|
||||
<option value="false">关闭,只记录警告</option>
|
||||
</select>
|
||||
defaultValue={config.subscriptionRiskAutoSuspend}
|
||||
trueLabel="开启自动封停"
|
||||
falseLabel="只记录警告"
|
||||
ariaLabel="自动暂停"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subscriptionRiskWindowHours">统计窗口(小时)</Label>
|
||||
@@ -436,15 +421,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="nodeAccessRiskEnabled">节点日志风控</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="nodeAccessRiskEnabled"
|
||||
name="nodeAccessRiskEnabled"
|
||||
defaultValue={String(config.nodeAccessRiskEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="true">开启,接收 Agent Xray 日志上报</option>
|
||||
<option value="false">关闭,只保留订阅接口风控</option>
|
||||
</select>
|
||||
defaultValue={config.nodeAccessRiskEnabled}
|
||||
trueLabel="接收日志"
|
||||
falseLabel="仅订阅风控"
|
||||
ariaLabel="节点日志风控"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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="space-y-2">
|
||||
<Label htmlFor="allowRegistration">开放注册</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="allowRegistration"
|
||||
name="allowRegistration"
|
||||
defaultValue={String(config.allowRegistration)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
defaultValue={config.allowRegistration}
|
||||
trueLabel="开放"
|
||||
falseLabel="关闭"
|
||||
ariaLabel="开放注册"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="requireInviteCode">注册必须邀请码</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="requireInviteCode"
|
||||
name="requireInviteCode"
|
||||
defaultValue={String(config.requireInviteCode)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="false">否</option>
|
||||
<option value="true">是</option>
|
||||
</select>
|
||||
defaultValue={config.requireInviteCode}
|
||||
trueLabel="必须"
|
||||
falseLabel="不需要"
|
||||
ariaLabel="注册必须邀请码"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="emailVerificationRequired">注册邮箱验证</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="emailVerificationRequired"
|
||||
name="emailVerificationRequired"
|
||||
defaultValue={String(config.emailVerificationRequired)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="false">关闭</option>
|
||||
<option value="true">开启,注册后必须验证邮箱</option>
|
||||
</select>
|
||||
defaultValue={config.emailVerificationRequired}
|
||||
trueLabel="开启验证"
|
||||
falseLabel="关闭"
|
||||
ariaLabel="注册邮箱验证"
|
||||
/>
|
||||
<p className="text-xs leading-5 text-muted-foreground">开启后,新用户注册会先收到验证邮件,完成验证后才能登录;关闭后注册成功即可登录。</p>
|
||||
</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="space-y-2">
|
||||
<Label htmlFor="smtpEnabled">邮件服务</Label>
|
||||
<select id="smtpEnabled" name="smtpEnabled" defaultValue={String(config.smtpEnabled)} className={selectClassName}>
|
||||
<option value="false">关闭</option>
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
<BooleanToggle
|
||||
id="smtpEnabled"
|
||||
name="smtpEnabled"
|
||||
defaultValue={config.smtpEnabled}
|
||||
ariaLabel="邮件服务"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtpHost">SMTP 主机</Label>
|
||||
@@ -540,10 +523,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtpSecure">TLS / SSL</Label>
|
||||
<select id="smtpSecure" name="smtpSecure" defaultValue={String(config.smtpSecure)} className={selectClassName}>
|
||||
<option value="false">STARTTLS / 普通连接</option>
|
||||
<option value="true">SSL 直连</option>
|
||||
</select>
|
||||
<BooleanToggle
|
||||
id="smtpSecure"
|
||||
name="smtpSecure"
|
||||
defaultValue={config.smtpSecure}
|
||||
trueLabel="SSL 直连"
|
||||
falseLabel="STARTTLS"
|
||||
ariaLabel="TLS / SSL"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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="space-y-2">
|
||||
<Label htmlFor="inviteRewardEnabled">自动发放奖励</Label>
|
||||
<select
|
||||
<BooleanToggle
|
||||
id="inviteRewardEnabled"
|
||||
name="inviteRewardEnabled"
|
||||
defaultValue={String(config.inviteRewardEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="false">关闭</option>
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
defaultValue={config.inviteRewardEnabled}
|
||||
ariaLabel="自动发放奖励"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteRewardRate">返利比例(%)</Label>
|
||||
|
||||
77
src/components/ui/boolean-toggle.tsx
Normal file
77
src/components/ui/boolean-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user