1
This commit is contained in:
56
src/components/bs-ui/accordion/index.tsx
Normal file
56
src/components/bs-ui/accordion/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cname("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"flex flex-1 items-center py-4 text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRightIcon color="#111" className="mx-2 h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
{children}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cname("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
140
src/components/bs-ui/alertDialog/index.tsx
Normal file
140
src/components/bs-ui/alertDialog/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cname(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-sm translate-x-[-50%] translate-y-[-50%] gap-10 border bg-background-prompt p-3 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cname(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cname(
|
||||
"flex justify-center flex-col-reverse sm:flex-row sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cname("text-lg font-semibold text-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cname("text-sm text-muted-foreground text-center text-prompt-description", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cname(buttonVariants({variant: "destructive"} ), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cname(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
94
src/components/bs-ui/alertDialog/useConfirm.tsx
Normal file
94
src/components/bs-ui/alertDialog/useConfirm.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "."
|
||||
import ReactDOM from "react-dom"
|
||||
import i18next from "i18next"
|
||||
import { useRef, useState } from "react"
|
||||
import { TipIcon } from "@/components/bs-icons/tip"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
interface ConfirmParams {
|
||||
title?: string
|
||||
desc: string | React.ReactNode
|
||||
canelTxt?: string
|
||||
okTxt?: string
|
||||
showClose?: boolean
|
||||
onClose?: () => void
|
||||
onCancel?: () => void
|
||||
onOk?: (next) => void
|
||||
}
|
||||
|
||||
let openFn = (_: ConfirmParams) => { }
|
||||
|
||||
function ConfirmWrapper() {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const paramRef = useRef(null)
|
||||
|
||||
openFn = (params: ConfirmParams) => {
|
||||
paramRef.current = params
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
paramRef.current?.onClose?.()
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleCancelClick = () => {
|
||||
paramRef.current?.onCancel?.()
|
||||
close()
|
||||
}
|
||||
|
||||
const handleOkClick = () => {
|
||||
paramRef.current?.onOk
|
||||
? paramRef.current?.onOk?.(close)
|
||||
: close()
|
||||
}
|
||||
|
||||
if (!paramRef.current) return null
|
||||
const { title, desc, okTxt, canelTxt, showClose = true } = paramRef.current
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader className="relative">
|
||||
<div><TipIcon /></div>
|
||||
{showClose && <Cross2Icon onClick={close} className="absolute right-0 top-[-0.5rem] cursor-pointer text-gray-400 hover:text-gray-600"></Cross2Icon>}
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{desc}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancelClick} className="px-11">{canelTxt}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleOkClick} className="px-11">{okTxt}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
(function () {
|
||||
// 挂载组件
|
||||
let el = document.getElementById('#confirm-wrap');
|
||||
if (!el) {
|
||||
el = document.createElement('div')
|
||||
el.id = 'confirm-wrap'
|
||||
document.body.append(el)
|
||||
}
|
||||
ReactDOM.render(<ConfirmWrapper />, el);
|
||||
})();
|
||||
|
||||
|
||||
const bsConfirm = (params: ConfirmParams) => {
|
||||
const resource = i18next.getResourceBundle(i18next.language, 'bs')
|
||||
|
||||
openFn({
|
||||
title: resource.prompt,
|
||||
canelTxt: resource.cancel,
|
||||
okTxt: resource.confirmButton,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
export { bsConfirm }
|
||||
35
src/components/bs-ui/badge/index.tsx
Normal file
35
src/components/bs-ui/badge/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> { }
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cname(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
45
src/components/bs-ui/button/actionButton.tsx
Normal file
45
src/components/bs-ui/button/actionButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { CaretDownIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from ".";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
||||
|
||||
export default function ActionButton({
|
||||
children,
|
||||
className = '',
|
||||
dropDown = null,
|
||||
align = 'center',
|
||||
buttonTipContent = null,
|
||||
delayDuration = 700,
|
||||
variant = "default",
|
||||
...props
|
||||
}) {
|
||||
|
||||
return <div className="flex items-center">
|
||||
<>
|
||||
{buttonTipContent ? <TooltipProvider>
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant={variant} className={`rounded-r-none ${className}`} {...props}>{children}</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[#fff] text-gray-800">
|
||||
{buttonTipContent}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider> :
|
||||
<Button variant={variant} className={`rounded-r-none ${className}`} {...props}>{children}</Button>
|
||||
}
|
||||
</>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant={variant}
|
||||
className="rounded-l-none ml-[1px] [&[data-state=open]>svg]:rotate-180"
|
||||
><CaretDownIcon /></Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0" align={align}>
|
||||
{dropDown}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
};
|
||||
103
src/components/bs-ui/button/index.tsx
Normal file
103
src/components/bs-ui/button/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cname } from "../utils"
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary no-underline hover:underline",
|
||||
black: "bg-[#111] text-primary-foreground shadow hover:bg-[#48494d]"
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cname(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
|
||||
const ButtonNumber = React.forwardRef<HTMLButtonElement, {
|
||||
className?: string,
|
||||
defaultValue?: number,
|
||||
value?: number,
|
||||
max?: number,
|
||||
min?: number,
|
||||
step?: number,
|
||||
size?: "default" | "sm" | "lg" | "icon",
|
||||
onChange?: (value: number) => void
|
||||
}>(({ className, value: userValue, defaultValue, max = 100, min = 0, step = 1, size = 'sm', onChange }, ref) => {
|
||||
if (max <= min) {
|
||||
throw new Error('max must be greater than min');
|
||||
}
|
||||
const [value, setValue] = React.useState(defaultValue)
|
||||
React.useEffect(() => {
|
||||
setValue(userValue)
|
||||
}, [userValue])
|
||||
|
||||
const getDecimalCount = (value: number) => {
|
||||
return (String(value).split('.')[1] || '').length;
|
||||
}
|
||||
const roundValue = (value: number, decimalCount: number) => {
|
||||
const rounder = Math.pow(10, decimalCount);
|
||||
return Math.round(value * rounder) / rounder;
|
||||
}
|
||||
const valueAdd = () => {
|
||||
const sum = roundValue(value + step, getDecimalCount(step))
|
||||
const updateValue = sum > max ? max : sum
|
||||
setValue(updateValue)
|
||||
onChange?.(updateValue)
|
||||
}
|
||||
const valueReduce = () => {
|
||||
const sum = roundValue(value - step, getDecimalCount(step))
|
||||
const updateValue = sum < min ? min : sum
|
||||
setValue(updateValue)
|
||||
onChange?.(updateValue)
|
||||
}
|
||||
return (<div className={cname("flex items-center border input-border bg-gray-50 rounded-md", className)}>
|
||||
<Button variant="ghost" size={size} disabled={value === min} onClick={valueReduce}>-</Button>
|
||||
<span className="min-w-10 block text-center">{value}</span>
|
||||
<Button variant="ghost" size={size} disabled={value === max} onClick={valueAdd}>+</Button>
|
||||
</div>)
|
||||
}
|
||||
)
|
||||
ButtonNumber.displayName = "ButtonNumber"
|
||||
|
||||
export { Button, ButtonNumber, buttonVariants }
|
||||
75
src/components/bs-ui/card/index.tsx
Normal file
75
src/components/bs-ui/card/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as React from "react"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"rounded-md bg-card text-card-foreground border border-transparent hover:shadow-[0_8px_16px_0px_rgba(40,47,84,0.15)] hover:border-[#EDF0F4] relative hover:z-10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cname("flex flex-col space-y-1.5 p-5 pb-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cname("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cname("text-sm text-muted-foreground ", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cname("p-5 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cname("flex items-center p-5 pb-4 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
29
src/components/bs-ui/checkBox/index.tsx
Normal file
29
src/components/bs-ui/checkBox/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cname } from "../utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cname("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
15
src/components/bs-ui/dialog/DialogForceUpdate.tsx
Normal file
15
src/components/bs-ui/dialog/DialogForceUpdate.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogTrigger } from ".";
|
||||
|
||||
// 强制刷新children的 dialog 组件
|
||||
export default function DialogForceUpdate({ children, trigger }) {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return <Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger}
|
||||
</DialogTrigger>
|
||||
{open ? children : null}
|
||||
</Dialog>
|
||||
};
|
||||
113
src/components/bs-ui/dialog/index.tsx
Normal file
113
src/components/bs-ui/dialog/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import * as React from "react"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-5 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cname(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cname(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"text-base font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cname("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog, DialogClose,
|
||||
DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger
|
||||
}
|
||||
|
||||
62
src/components/bs-ui/editLabel/index.tsx
Normal file
62
src/components/bs-ui/editLabel/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { PenLine } from "lucide-react";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
|
||||
export default function EditLabel({ str, rule, children, onChange }) {
|
||||
const [edit, setEdit] = useState(false)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
const handleChange = () => {
|
||||
const value = inputRef.current.value
|
||||
if (rule.length) {
|
||||
const errors = []
|
||||
rule.forEach(r => {
|
||||
if (r.pattern) {
|
||||
if (!r.pattern.test(value)) {
|
||||
errors.push(r.message)
|
||||
}
|
||||
}
|
||||
if (r.validator) {
|
||||
if (!r.validator(value)) {
|
||||
errors.push(r.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (errors.length) return setErrorData({
|
||||
title: "",
|
||||
list: errors,
|
||||
});
|
||||
}
|
||||
setEdit(false)
|
||||
onChange(value)
|
||||
}
|
||||
|
||||
if (edit) return <div className="">
|
||||
<input
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
defaultValue={str}
|
||||
onKeyDown={(e) => {
|
||||
e.key === 'Enter' && handleChange();
|
||||
e.code === 'Space' && e.preventDefault();
|
||||
}}
|
||||
onBlur={handleChange}
|
||||
className="flex h-6 w-full rounded-xl border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
return <div className="flex items-center text-gray-900 dark:text-gray-300 group">
|
||||
{children(inputRef.current?.value || str)}
|
||||
<button
|
||||
className="hidden transition-all group-hover:block"
|
||||
// title={t('flow.editAlias')}
|
||||
onClick={() => setEdit(true)}
|
||||
>
|
||||
<PenLine size={18} className="ml-2 cursor-pointer" />
|
||||
</button>
|
||||
</div >
|
||||
};
|
||||
147
src/components/bs-ui/input/index.tsx
Normal file
147
src/components/bs-ui/input/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as React from "react"
|
||||
import { cname } from "../utils"
|
||||
import { SearchIcon } from "../../bs-icons/search"
|
||||
import { generateUUID } from "../utils"
|
||||
import { MinusCircledIcon } from "@radix-ui/react-icons"
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> { }
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cname(
|
||||
"flex h-9 w-full rounded-md border border-input bg-[#FAFBFC] px-3 py-1 text-sm text-[#111] shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
|
||||
const SearchInput = React.forwardRef<HTMLInputElement, InputProps & { inputClassName?: string, iconClassName?: string }>(
|
||||
({ className, inputClassName, iconClassName, ...props }, ref) => {
|
||||
return <div className={cname("relative", className)}>
|
||||
<SearchIcon className={cname("h-5 w-5 absolute left-2 top-2", iconClassName)} />
|
||||
<Input type="text" ref={ref} className={cname("pl-8 bg-search-input", inputClassName)} {...props}></Input>
|
||||
</div>
|
||||
}
|
||||
)
|
||||
|
||||
SearchInput.displayName = "SearchInput"
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 多行文本
|
||||
*/
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cname(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-[#FAFBFC] px-3 py-2 text-sm text-[#111] shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
|
||||
/**
|
||||
* input list
|
||||
*/
|
||||
const InputList = React.forwardRef<HTMLDivElement, InputProps & {
|
||||
rules: any[],
|
||||
value?: string[],
|
||||
inputClassName?: string,
|
||||
defaultValue?: string[],
|
||||
onChange?: (values: string[]) => void
|
||||
}>(
|
||||
({ rules, className, inputClassName, value = [], defaultValue = [], ...props }, ref) => {
|
||||
// 初始化 inputs 状态,为每个值分配唯一 ID
|
||||
const [inputs, setInputs] = React.useState(() =>
|
||||
value.map(val => ({ id: generateUUID(8), value: val }))
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// 仅为新增的值分配新的 ID
|
||||
const updatedInputs = value.map((val, index) => {
|
||||
return inputs[index] && inputs[index].value === val
|
||||
? inputs[index] // 如果当前输入项与外部值相同,则保持不变
|
||||
: { id: generateUUID(8), value: val }; // 否则,创建新的输入项
|
||||
});
|
||||
setInputs(updatedInputs);
|
||||
}, [value]); // 依赖项中包含 value,确保外部 value 更新时同步更新
|
||||
|
||||
|
||||
const handleChange = (newValue, id, index) => {
|
||||
let newInputs = inputs.map(input =>
|
||||
input.id === id ? { ...input, value: newValue } : input
|
||||
);
|
||||
// push
|
||||
if (index === newInputs.length - 1) {
|
||||
newInputs = ([...newInputs, { id: generateUUID(8), value: '' }]);
|
||||
}
|
||||
setInputs(newInputs);
|
||||
props.onChange(newInputs.map(input => input.value));
|
||||
};
|
||||
|
||||
// delete input
|
||||
const handleRemoveInput = (id) => {
|
||||
const newInputs = inputs.filter(input => input.id !== id);
|
||||
setInputs(newInputs);
|
||||
props.onChange(newInputs.map(input => input.value));
|
||||
};
|
||||
|
||||
return <div className={cname('', className)}>
|
||||
{
|
||||
inputs.map((item, index) => (
|
||||
<div className="relative mt-2">
|
||||
<Input
|
||||
key={item.id}
|
||||
defaultValue={item.value}
|
||||
className={cname('pr-8', inputClassName)}
|
||||
placeholder={props.placeholder || ''}
|
||||
onChange={(e) => handleChange(e.target.value, item.id, index)}
|
||||
onInput={(e) => {
|
||||
rules.some(rule => {
|
||||
if (rule.maxLength && e.target.value.length > rule.maxLength) {
|
||||
e.target.nextSibling.textContent = rule.message;
|
||||
e.target.nextSibling.style.display = '';
|
||||
return true;
|
||||
}
|
||||
e.target.nextSibling.style.display = 'none';
|
||||
})
|
||||
}}
|
||||
// onFocus={(e) => {
|
||||
// if (e.target.value && index === inputs.length - 1) {
|
||||
// setInputs([...inputs, { id: generateUUID(8), value: '' }]);
|
||||
// }
|
||||
// }}
|
||||
></Input>
|
||||
<p className="text-sm text-red-500" style={{ display: 'none' }}></p>
|
||||
{index !== inputs.length - 1 && <MinusCircledIcon onClick={(e) => {
|
||||
e.target.previousSibling.style.display = 'none';
|
||||
handleRemoveInput(item.id)
|
||||
}} className="absolute top-2.5 right-2 text-gray-500 hover:text-gray-700 cursor-pointer" />}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
)
|
||||
|
||||
export { Input, SearchInput, Textarea, InputList }
|
||||
31
src/components/bs-ui/input/textInput.tsx
Normal file
31
src/components/bs-ui/input/textInput.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState } from "react"
|
||||
import { Input } from "."
|
||||
|
||||
export default function TextInput({
|
||||
type = 'doubleclick',
|
||||
value,
|
||||
onChange = (val) => { },
|
||||
onSave = (val) => { },
|
||||
...props }) {
|
||||
|
||||
const [edit, setEdit] = useState(false)
|
||||
|
||||
if (edit) return <Input defaultValue={value} {...props}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
onSave(event.target.value)
|
||||
setEdit(false)
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
onSave(e.target.value)
|
||||
setEdit(false)
|
||||
}}
|
||||
onChange={onChange}
|
||||
></Input>
|
||||
|
||||
return <p
|
||||
className="text-sm px-3 py-1 border border-transparent w-full overflow-hidden text-ellipsis"
|
||||
onDoubleClick={() => setEdit(true)} onMouseOver={() => type === 'hover' && setEdit(true)}>{value}</p>
|
||||
};
|
||||
25
src/components/bs-ui/label/index.tsx
Normal file
25
src/components/bs-ui/label/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cname } from "../utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cname(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
127
src/components/bs-ui/pagination/autoPagination.tsx
Normal file
127
src/components/bs-ui/pagination/autoPagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons';
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from './index';
|
||||
|
||||
interface IProps {
|
||||
/** 当前页码 */
|
||||
page: number,
|
||||
/** limit */
|
||||
pageSize: number,
|
||||
/** item total */
|
||||
total: number,
|
||||
/** 最多同时显示 item 数 */
|
||||
maxVisiblePages?: number,
|
||||
onChange?: (p: number) => void,
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AutoPagination = ({ page, pageSize, total, maxVisiblePages = 5, className, onChange }: IProps) => {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
if (newPage >= 1 && newPage <= totalPages && newPage !== page) {
|
||||
onChange?.(newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPaginationItems = () => {
|
||||
const items = [];
|
||||
|
||||
// If total pages are more than maxVisiblePages, show at most maxVisiblePages pages
|
||||
const startPage = Math.max(1, page - Math.floor(maxVisiblePages / 2));
|
||||
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
// page 0
|
||||
if (page !== 1) {
|
||||
items.push(
|
||||
<PaginationItem key="start">
|
||||
<PaginationLink href="#" onClick={() => handlePageChange(1)} >
|
||||
<DoubleArrowLeftIcon />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
}
|
||||
// Previous Button
|
||||
items.push(
|
||||
<PaginationItem key="previous">
|
||||
<PaginationPrevious href="#"
|
||||
className={page === startPage && 'text-gray-400'}
|
||||
onClick={() => handlePageChange(page - 1)} />
|
||||
</PaginationItem>
|
||||
);
|
||||
|
||||
// Page Buttons
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// If total pages are less than or equal to maxVisiblePages, show all pages
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink href="#"
|
||||
className={page === i ? 'font-bold' : 'text-gray-500'}
|
||||
onClick={() => handlePageChange(i)} isActive={i === page}>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
// if (startPage > 1) {
|
||||
// // Display ellipsis if there are pages before the startPage
|
||||
// items.push(
|
||||
// <PaginationItem key="startEllipsis">
|
||||
// <PaginationEllipsis />
|
||||
// </PaginationItem>
|
||||
// );
|
||||
// }
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink href="#"
|
||||
className={page === i ? 'font-bold' : 'text-gray-500'}
|
||||
onClick={() => handlePageChange(i)} isActive={i === page}>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
}
|
||||
|
||||
// if (endPage < totalPages) {
|
||||
// // Display ellipsis if there are pages after the endPage
|
||||
// items.push(
|
||||
// <PaginationItem key="endEllipsis">
|
||||
// <PaginationEllipsis />
|
||||
// </PaginationItem>
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
// Next Button
|
||||
items.push(
|
||||
<PaginationItem key="next">
|
||||
<PaginationNext href="#"
|
||||
className={page === endPage && 'text-gray-400'}
|
||||
onClick={() => handlePageChange(page + 1)} />
|
||||
</PaginationItem>
|
||||
);
|
||||
// page last
|
||||
if (page !== totalPages) {
|
||||
items.push(
|
||||
<PaginationItem key="end">
|
||||
<PaginationLink href="#" onClick={() => handlePageChange(totalPages)} >
|
||||
<DoubleArrowRightIcon />
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<Pagination className={className}>
|
||||
<PaginationContent>{renderPaginationItems()}</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoPagination;
|
||||
124
src/components/bs-ui/pagination/index.tsx
Normal file
124
src/components/bs-ui/pagination/index.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DotsHorizontalIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
|
||||
import { ButtonProps, buttonVariants } from "../button"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cname("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cname("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cname("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cname(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
"text-gray-950",
|
||||
"dark:text-[#585858]",
|
||||
isActive && "dark:bg-[#34353A] dark:text-[#F2F2F2]", // 暗黑模式设计
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cname("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
{/* <span>Previous</span> */}
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cname("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{/* <span>Next</span> */}
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cname("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="h-4 w-4" />
|
||||
{/* <span className="sr-only">More pages</span> */}
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis
|
||||
}
|
||||
34
src/components/bs-ui/popover/index.tsx
Normal file
34
src/components/bs-ui/popover/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverClose = PopoverPrimitive.Close
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cname(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose }
|
||||
44
src/components/bs-ui/radio/index.tsx
Normal file
44
src/components/bs-ui/radio/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon } from "@radix-ui/react-icons"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { cname } from "../utils"
|
||||
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cname("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"aspect-square h-4 w-4 rounded-full border-2 border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<span className="w-1.5 h-1.5 bg-primary rounded-full"></span>
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
15
src/components/bs-ui/radio/radioCard.tsx
Normal file
15
src/components/bs-ui/radio/radioCard.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { cname } from "../utils";
|
||||
|
||||
export default function RadioCard({ calssName = '', checked, title, description = '' }) {
|
||||
|
||||
return <div className={cname('w-96 border rounded-sm flex gap-2 p-4 cursor-pointer transition-all hover:border-primary/50', calssName, checked && 'border-primary bg-primary/10')}>
|
||||
<div className={`bg-[#fff] border border-gray-400 rounded-full w-5 h-5 min-w-5 ${checked && 'bg-primary flex justify-center items-center'}`}>
|
||||
{checked && <div className="w-2 h-2 bg-gray-50 rounded-full" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium leading-none mt-0.5 mb-2">{title}</div>
|
||||
<div className="text-sm text-muted-foreground">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
157
src/components/bs-ui/select/index.tsx
Normal file
157
src/components/bs-ui/select/index.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon
|
||||
} from "@radix-ui/react-icons"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import * as React from "react"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"group flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-[#fcfdff] px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 data-[placeholder]:text-gray-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<CaretDownIcon className="h-5 w-5 min-w-5 opacity-80 group-data-[state=open]:rotate-180" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, headNode = null, footerNode = null, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-[#DEE3EF] bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
{headNode}
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cname(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
{footerNode}
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cname("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-[#EBF0FF] focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cname("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue
|
||||
}
|
||||
|
||||
133
src/components/bs-ui/select/multi.tsx
Normal file
133
src/components/bs-ui/select/multi.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { CheckIcon, Cross1Icon } from "@radix-ui/react-icons"
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "."
|
||||
import { Badge } from "../badge"
|
||||
import { SearchInput } from "../input"
|
||||
|
||||
const MultiItem = ({ active, children, value, onClick }) => {
|
||||
|
||||
return <div key={value}
|
||||
className={`relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 mb-1 text-sm outline-none hover:bg-[#EBF0FF] hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 break-all
|
||||
${active && 'bg-[#EBF0FF]'}`}
|
||||
onClick={() => { onClick(value) }}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
{active && <CheckIcon className="h-4 w-4"></CheckIcon>}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
interface IProps {
|
||||
className?: string,
|
||||
options: { label: string, value: string }[],
|
||||
value?: string[],
|
||||
defaultValue?: string[],
|
||||
children?: React.ReactNode,
|
||||
placeholder?: string,
|
||||
searchPlaceholder?: string,
|
||||
lockedValues?: string[],
|
||||
onChange?: (value: string[]) => void
|
||||
}
|
||||
// 临时用 andt 设计方案封装组件
|
||||
const MultiSelect = ({
|
||||
className,
|
||||
value = [],
|
||||
defaultValue = [],
|
||||
options = [],
|
||||
children = null,
|
||||
placeholder = '',
|
||||
searchPlaceholder = '',
|
||||
lockedValues = [],
|
||||
onChange, ...props
|
||||
}: IProps) => {
|
||||
|
||||
const [values, setValues] = React.useState(defaultValue)
|
||||
const [optionFilter, setOptionFilter] = React.useState(options)
|
||||
|
||||
|
||||
const inputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
setValues(value)
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
setOptionFilter(options)
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = ''
|
||||
}
|
||||
}, [options])
|
||||
// delete
|
||||
const handleDelete = (value: string) => {
|
||||
const newValues = values.filter((item) => {
|
||||
return item !== value
|
||||
})
|
||||
setValues(newValues)
|
||||
onChange?.(newValues)
|
||||
}
|
||||
// add
|
||||
const handleSwitch = (value: string) => {
|
||||
if (lockedValues.includes(value)) {
|
||||
return
|
||||
}
|
||||
if (values.includes(value)) {
|
||||
const newValues = values.filter((item) => {
|
||||
return item !== value
|
||||
})
|
||||
setValues(newValues)
|
||||
onChange?.(newValues)
|
||||
} else {
|
||||
const _newValues = [...values, value]
|
||||
setValues(_newValues)
|
||||
onChange?.(_newValues)
|
||||
}
|
||||
}
|
||||
|
||||
// search
|
||||
const handleSearch = (e) => {
|
||||
const newValues = options.filter((item) => {
|
||||
return item.label.toLowerCase().indexOf(e.target.value.toLowerCase()) !== -1
|
||||
})
|
||||
setOptionFilter(newValues)
|
||||
}
|
||||
return <Select {...props} required onOpenChange={(e) => !e && setOptionFilter(options)}>
|
||||
<SelectTrigger className="mt-2 h-auto">
|
||||
{
|
||||
values.length
|
||||
? <div className="flex flex-wrap">
|
||||
{
|
||||
options.filter(option => values.includes(option.value)).map(option =>
|
||||
<Badge onPointerDown={(e) => e.stopPropagation()} key={option.value} className="flex whitespace-normal items-center gap-1 select-none bg-primary/20 text-primary hover:bg-primary/15 m-[2px]">
|
||||
{option.label}
|
||||
{lockedValues.includes(option.value) || <Cross1Icon className="h-3 w-3" onClick={() => handleDelete(option.value)}></Cross1Icon>}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
: placeholder
|
||||
}
|
||||
</SelectTrigger>
|
||||
<SelectContent className={className}
|
||||
headNode={
|
||||
<div className="p-2">
|
||||
<SearchInput ref={inputRef} inputClassName="h-8" placeholder={searchPlaceholder} onChange={handleSearch} iconClassName="w-4 h-4" />
|
||||
</div>
|
||||
}
|
||||
footerNode={children}
|
||||
>
|
||||
<div className="mt-2">
|
||||
{
|
||||
optionFilter.map((item, index) => (
|
||||
<MultiItem active={values.includes(item.value)} value={item.value} onClick={handleSwitch}>{item.label}</MultiItem>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
|
||||
MultiSelect.displayName = 'MultiSelect'
|
||||
|
||||
export default MultiSelect
|
||||
140
src/components/bs-ui/sheet/index.tsx
Normal file
140
src/components/bs-ui/sheet/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cname(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> { }
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cname(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cname(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cname(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cname("text-lg font-semibold text-foreground mb-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cname("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
14
src/components/bs-ui/skeleton/index.tsx
Normal file
14
src/components/bs-ui/skeleton/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { cname } from "../utils";
|
||||
|
||||
export default function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cname("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
};
|
||||
28
src/components/bs-ui/slider/index.tsx
Normal file
28
src/components/bs-ui/slider/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import { cname } from "../utils"
|
||||
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
30
src/components/bs-ui/switch/index.tsx
Normal file
30
src/components/bs-ui/switch/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, texts = null, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cname(
|
||||
"group peer relative inline-flex h-5 min-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}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cname(
|
||||
"pointer-events-none block h-3.5 min-w-3.5 w-3.5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:ml-[100%] data-[state=checked]:translate-x-[calc(-50%-8px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
{texts && <>
|
||||
<span className="text text-xs absolute left-1 text-gray-50 hidden group-data-[state=checked]:block">{texts[0]}</span>
|
||||
<span className="text text-xs absolute right-1 text-gray-400 hidden group-data-[state=unchecked]:block">{texts[1]}</span>
|
||||
</>}
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
119
src/components/bs-ui/table/index.tsx
Normal file
119
src/components/bs-ui/table/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import * as React from "react"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cname("w-full caption-bottom text-sm border-separate border-spacing-y-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cname("[&>tr]:first:bg-transparent [&>tr]:first:border-none", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cname("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"group transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground text-md [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] bg-[#FBFBFB] first:rounded-l-md last:rounded-r-md group-odd:bg-[#f4f5f8] group-hover:bg-[#ebf0ff]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cname("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
54
src/components/bs-ui/tabs/index.tsx
Normal file
54
src/components/bs-ui/tabs/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
import { cname } from "../utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:border data-[state=inactive]:border data-[state=inactive]:border-muted data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=inactive]:hover:bg-secondary/80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
37
src/components/bs-ui/toast/index.tsx
Normal file
37
src/components/bs-ui/toast/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ToastIcon } from "../../bs-icons/toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "./toast"
|
||||
import { useToast } from "./use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props} duration={2600}>
|
||||
<div className=" self-start">
|
||||
<ToastIcon type={props.variant} />
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
134
src/components/bs-ui/toast/toast.tsx
Normal file
134
src/components/bs-ui/toast/toast.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cname } from "../utils"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"fixed top-0 z-[1000] flex w-full flex-col-reverse pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full md:w-[420px] items-center space-x-3 overflow-hidden rounded-md border p-4 mt-4 mr-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-top-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
info: "info border-[#024FE5] bg-[#F1F6FF] self-end",
|
||||
success: "success border-[#0BA95D] bg-[#F2FFF9] self-end",
|
||||
warning: "warning border-[#EA991F] bg-[#FFF7EC] self-end",
|
||||
error: "error border-[#D8341E] bg-[#FFF2F0] self-end",
|
||||
},
|
||||
message: {
|
||||
info: "shadow-xl bg-[#fff] self-center",
|
||||
success: "shadow-xl bg-[#fff] self-center",
|
||||
warning: "shadow-xl bg-[#fff] self-center",
|
||||
error: "shadow-xl bg-[#fff] self-center",
|
||||
}
|
||||
},
|
||||
defaultVariants: {},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants> &
|
||||
{ isAlert?: boolean }
|
||||
>(({ className, variant = 'info', isAlert, ...props }, ref) => {
|
||||
const variants = isAlert ? { message: variant } : { variant }
|
||||
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cname(toastVariants(variants), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cname(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cname("text-sm font-semibold [&+div]:text-xs group-[.info]:text-[#024FE5] group-[.success]:text-[#0BA95D] group-[.warning]:text-[#EA991F] group-[.error]:text-[#D8341E]", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cname("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
202
src/components/bs-ui/toast/use-toast.tsx
Normal file
202
src/components/bs-ui/toast/use-toast.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "./toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 6 * 1000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
isAlert?: boolean,
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id" | "isAlert"> & { description: string[] | string }
|
||||
|
||||
function toast({ description: descs = [], ...props }: Toast & { isAlert?: boolean }) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
const description = Array.isArray(descs) ? descs.map(msg => <p>{msg}</p>) : descs
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
description,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
console.log('toast remove', id, open);
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function message(props: Toast) {
|
||||
return toast({ ...props, isAlert: true })
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
message,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
// message中间提示,toast右侧告警
|
||||
export { useToast, toast, message }
|
||||
29
src/components/bs-ui/tooltip/index.tsx
Normal file
29
src/components/bs-ui/tooltip/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import { cname } from "../utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cname(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
20
src/components/bs-ui/utils.tsx
Normal file
20
src/components/bs-ui/utils.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* 样式合并
|
||||
*/
|
||||
export function cname(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
|
||||
export const generateUUID = (length: number) => {
|
||||
let d = new Date().getTime()
|
||||
const uuid = ''.padStart(length, 'x').replace(/[xy]/g, (c) => {
|
||||
const r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16)
|
||||
})
|
||||
return uuid
|
||||
}
|
||||
Reference in New Issue
Block a user