From 79b0100d98756db703fb48eb3c094aa397df6f67 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Tue, 10 Mar 2026 08:20:31 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=B0=83=E6=95=B4=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E5=88=86=E7=BB=84=E5=BC=B9=E6=A1=86=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/GroupModal.jsx | 127 +++++++++++------- app/globals.css | 8 +- components/ui/button.jsx | 60 +++++++++ components/ui/dialog.jsx | 2 +- components/ui/field.jsx | 245 ++++++++++++++++++++++++++++++++++ components/ui/label.jsx | 23 ++++ components/ui/separator.jsx | 27 ++++ package-lock.json | 2 +- public/manifest.webmanifest | 34 +++++ 9 files changed, 474 insertions(+), 54 deletions(-) create mode 100644 components/ui/button.jsx create mode 100644 components/ui/field.jsx create mode 100644 components/ui/label.jsx create mode 100644 components/ui/separator.jsx create mode 100644 public/manifest.webmanifest diff --git a/app/components/GroupModal.jsx b/app/components/GroupModal.jsx index 6fa9f9b..154f7be 100644 --- a/app/components/GroupModal.jsx +++ b/app/components/GroupModal.jsx @@ -1,61 +1,92 @@ 'use client'; import { useState } from 'react'; -import { motion } from 'framer-motion'; -import { CloseIcon, PlusIcon } from './Icons'; +import { Dialog, DialogContent, DialogTitle, DialogFooter, DialogClose } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Field, FieldLabel, FieldContent } from '@/components/ui/field'; +import { PlusIcon, CloseIcon } from './Icons'; +import { cn } from '@/lib/utils'; export default function GroupModal({ onClose, onConfirm }) { const [name, setName] = useState(''); + return ( - { + if (!open) onClose?.(); + }} > - e.stopPropagation()} + -
-
- - 新增分组 +
+
+
+ + + 新增分组 + +
+ + + +
+ + + + 分组名称(最多 8 个字) + + + { + const v = e.target.value || ''; + setName(v.slice(0, 8)); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && name.trim()) onConfirm(name.trim()); + }} + /> + + + +
+ +
-
-
- - { - const v = e.target.value || ''; - // 限制最多 8 个字符(兼容中英文),超出部分自动截断 - setName(v.slice(0, 8)); - }} - onKeyDown={(e) => { - if (e.key === 'Enter' && name.trim()) onConfirm(name.trim()); - }} - /> -
-
- - -
- - + + ); } diff --git a/app/globals.css b/app/globals.css index 0248805..6dda2b2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -23,12 +23,12 @@ --popover: #111827; --popover-foreground: #e5e7eb; --primary-foreground: #0f172a; - --secondary: #1f2937; + --secondary: #0b1220; --secondary-foreground: #e5e7eb; --muted-foreground: #9ca3af; --accent-foreground: #e5e7eb; --destructive: #f87171; - --input: #1f2937; + --input: #0b1220; --ring: #22d3ee; --chart-1: #22d3ee; --chart-2: #60a5fa; @@ -76,7 +76,7 @@ --muted-foreground: #475569; --accent-foreground: #ffffff; --destructive: #dc2626; - --input: #e2e8f0; + --input: #f1f5f9; --ring: #0891b2; --chart-1: #0891b2; --chart-2: #2563eb; @@ -3456,7 +3456,7 @@ input[type="number"] { --accent-foreground: #f8fafc; --destructive: #f87171; --border: #1f2937; - --input: #1e293b; + --input: #0b1220; --ring: #22d3ee; --chart-1: #22d3ee; --chart-2: #60a5fa; diff --git a/components/ui/button.jsx b/components/ui/button.jsx new file mode 100644 index 0000000..143adf0 --- /dev/null +++ b/components/ui/button.jsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { cva } from "class-variance-authority"; +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ); +} + +export { Button, buttonVariants } diff --git a/components/ui/dialog.jsx b/components/ui/dialog.jsx index 6f58c3f..811365d 100644 --- a/components/ui/dialog.jsx +++ b/components/ui/dialog.jsx @@ -58,7 +58,7 @@ function DialogContent({ diff --git a/components/ui/field.jsx b/components/ui/field.jsx new file mode 100644 index 0000000..35910c3 --- /dev/null +++ b/components/ui/field.jsx @@ -0,0 +1,245 @@ +"use client" + +import { useMemo } from "react" +import { cva } from "class-variance-authority"; + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ + className, + ...props +}) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} /> + ); +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}) { + return ( + + ); +} + +function FieldGroup({ + className, + ...props +}) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} /> + ); +} + +const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, +}) + +function Field({ + className, + orientation = "vertical", + ...props +}) { + return ( +
+ ); +} + +function FieldContent({ + className, + ...props +}) { + return ( +
+ ); +} + +function FieldLabel({ + className, + ...props +}) { + return ( +