refactor: Migrate documentation content, rebuild UI components, and update core architecture.
This commit is contained in:
21
components/markdown/AccordionContext.tsx
Normal file
21
components/markdown/AccordionContext.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createContext, useState, useId } from "react"
|
||||
|
||||
type AccordionGroupContextType = {
|
||||
inGroup: boolean
|
||||
groupId: string
|
||||
openTitle: string | null
|
||||
setOpenTitle: (title: string | null) => void
|
||||
}
|
||||
|
||||
export const AccordionGroupContext = createContext<AccordionGroupContextType | null>(null)
|
||||
|
||||
export function AccordionGroupProvider({ children }: { children: React.ReactNode }) {
|
||||
const [openTitle, setOpenTitle] = useState<string | null>(null)
|
||||
const groupId = useId()
|
||||
|
||||
return (
|
||||
<AccordionGroupContext.Provider value={{ inGroup: true, groupId, openTitle, setOpenTitle }}>
|
||||
{children}
|
||||
</AccordionGroupContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { AccordionGroupContext } from "@/components/contexts/AccordionContext";
|
||||
import React, { ReactNode } from "react"
|
||||
import clsx from "clsx"
|
||||
import { AccordionGroupProvider } from "@/components/markdown/AccordionContext"
|
||||
|
||||
interface AccordionGroupProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccordionGroup: React.FC<AccordionGroupProps> = ({ children, className }) => {
|
||||
|
||||
return (
|
||||
// Wrap all children with the AccordionGroupContext.Provider
|
||||
// so that any nested accordions know they are inside a group.
|
||||
// This enables group-specific behavior in child components.
|
||||
<AccordionGroupContext.Provider value={{ inGroup: true }}>
|
||||
<div
|
||||
className={clsx(
|
||||
"border rounded-lg overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
<AccordionGroupProvider>
|
||||
<div className={clsx("overflow-hidden rounded-lg border", className)}>{children}</div>
|
||||
</AccordionGroupProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccordionGroup;
|
||||
export default AccordionGroup
|
||||
|
||||
@@ -1,62 +1,61 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { ReactNode, useState, useContext } from 'react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import * as Icons from "lucide-react";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AccordionGroupContext } from '@/components/contexts/AccordionContext';
|
||||
import { ReactNode, useContext, useState } from "react"
|
||||
import { ChevronRight } from "lucide-react"
|
||||
import * as Icons from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { AccordionGroupContext } from "@/components/markdown/AccordionContext"
|
||||
|
||||
type AccordionProps = {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
icon?: keyof typeof Icons;
|
||||
};
|
||||
title: string
|
||||
children?: ReactNode
|
||||
icon?: keyof typeof Icons
|
||||
}
|
||||
|
||||
const Accordion: React.FC<AccordionProps> = ({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
icon,
|
||||
}: AccordionProps) => {
|
||||
const groupContext = useContext(AccordionGroupContext);
|
||||
const isInGroup = groupContext?.inGroup === true;
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null;
|
||||
const Accordion: React.FC<AccordionProps> = ({ title, children, icon }: AccordionProps) => {
|
||||
const groupContext = useContext(AccordionGroupContext)
|
||||
const isInGroup = groupContext?.inGroup === true
|
||||
const groupOpen = groupContext?.openTitle === title
|
||||
const setGroupOpen = groupContext?.setOpenTitle
|
||||
const [localOpen, setLocalOpen] = useState(false)
|
||||
|
||||
// The main wrapper div for the accordion.
|
||||
// All styling logic for the accordion container is handled here.
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// Style for STANDALONE: full card with border & shadow
|
||||
!isInGroup && "border rounded-lg shadow-sm",
|
||||
// Style for IN GROUP: only a bottom border separator
|
||||
isInGroup && "border-b last:border-b-0 border-border"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 w-full px-4 h-12 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"w-4 h-4 text-muted-foreground transition-transform duration-200 flex-shrink-0",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
{Icon && <Icon className="text-foreground w-4 h-4 flex-shrink-0" />}
|
||||
<h3 className="font-medium text-base text-foreground !m-0 leading-none">{title}</h3>
|
||||
</button>
|
||||
const isOpen = isInGroup ? groupOpen : localOpen
|
||||
|
||||
{isOpen && (
|
||||
<div className="px-4 py-3 dark:bg-muted/10 bg-muted/15">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const handleToggle = () => {
|
||||
if (isInGroup && setGroupOpen) {
|
||||
setGroupOpen(groupOpen ? null : title)
|
||||
} else {
|
||||
setLocalOpen(!localOpen)
|
||||
}
|
||||
}
|
||||
|
||||
export default Accordion;
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
!isInGroup && "rounded-lg border shadow-sm",
|
||||
isInGroup && "border-border border-b last:border-b-0"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
className="bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70 flex w-full cursor-pointer items-center gap-2 px-4 py-3 text-start transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
{Icon && <Icon className="text-foreground h-4 w-4 shrink-0" />}
|
||||
<h3 className="text-foreground m-0! text-base font-medium">{title}</h3>
|
||||
</button>
|
||||
|
||||
{isOpen && <div className="dark:bg-muted/10 bg-muted/15 px-4 py-3">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Accordion
|
||||
|
||||
@@ -24,13 +24,13 @@ const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, cl
|
||||
"bg-card text-card-foreground border-border",
|
||||
"hover:bg-accent/5 hover:border-accent/30",
|
||||
"flex gap-2",
|
||||
horizontal ? "flex-row items-center gap-1" : "flex-col space-y-1",
|
||||
horizontal ? "flex-row items-start gap-1" : "flex-col space-y-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className="w-5 h-5 text-primary flex-shrink-0" />}
|
||||
<div className="flex-1 min-w-0 my-auto h-full">
|
||||
<span className="text-base font-semibold text-foreground">{title}</span>
|
||||
{Icon && <Icon className={clsx("w-5 h-5 text-primary shrink-0", horizontal && "mt-0.5")} />}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base font-semibold text-foreground leading-6">{title}</div>
|
||||
<div className="text-sm text-muted-foreground -mt-3">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Copy({ content }: { content: string }) {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="border"
|
||||
className="border cursor-copy"
|
||||
size="xs"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ const FileComponent = ({ name }: FileProps) => {
|
||||
tabIndex={-1}
|
||||
>
|
||||
<FileIcon className={`
|
||||
h-3.5 w-3.5 flex-shrink-0 transition-colors
|
||||
h-3.5 w-3.5 shrink-0 transition-colors
|
||||
${isHovered ? 'text-accent' : 'text-muted-foreground'}
|
||||
`} />
|
||||
<span className="font-mono text-sm text-foreground truncate">{name}</span>
|
||||
@@ -61,7 +61,7 @@ const FolderComponent = ({ name, children }: FileProps) => {
|
||||
{hasChildren ? (
|
||||
<ChevronRight
|
||||
className={`
|
||||
h-3.5 w-3.5 flex-shrink-0 transition-transform duration-200
|
||||
h-3.5 w-3.5 shrink-0 transition-transform duration-200
|
||||
${isOpen ? 'rotate-90' : ''}
|
||||
${isHovered ? 'text-foreground/70' : 'text-muted-foreground'}
|
||||
`}
|
||||
@@ -71,12 +71,12 @@ const FolderComponent = ({ name, children }: FileProps) => {
|
||||
)}
|
||||
{isOpen ? (
|
||||
<FolderOpen className={`
|
||||
h-4 w-4 flex-shrink-0 transition-colors
|
||||
h-4 w-4 shrink-0 transition-colors
|
||||
${isHovered ? 'text-accent' : 'text-muted-foreground'}
|
||||
`} />
|
||||
) : (
|
||||
<FolderIcon className={`
|
||||
h-4 w-4 flex-shrink-0 transition-colors
|
||||
h-4 w-4 shrink-0 transition-colors
|
||||
${isHovered ? 'text-accent/80' : 'text-muted-foreground/80'}
|
||||
`} />
|
||||
)}
|
||||
|
||||
@@ -1,29 +1,129 @@
|
||||
import { ComponentProps } from "react";
|
||||
"use client";
|
||||
|
||||
import { ComponentProps, useState, useEffect } from "react";
|
||||
import NextImage from "next/image";
|
||||
import { createPortal } from "react-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, ZoomIn } from "lucide-react";
|
||||
|
||||
type Height = ComponentProps<typeof NextImage>["height"];
|
||||
type Width = ComponentProps<typeof NextImage>["width"];
|
||||
|
||||
type ImageProps = Omit<ComponentProps<"img">, "src"> & {
|
||||
src?: ComponentProps<typeof NextImage>["src"];
|
||||
src?: ComponentProps<typeof NextImage>["src"];
|
||||
};
|
||||
|
||||
export default function Image({
|
||||
src,
|
||||
alt = "alt",
|
||||
width = 800,
|
||||
height = 350,
|
||||
...props
|
||||
src,
|
||||
alt = "alt",
|
||||
width = 800,
|
||||
height = 350,
|
||||
...props
|
||||
}: ImageProps) {
|
||||
if (!src) return null;
|
||||
return (
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width as Width}
|
||||
height={height as Height}
|
||||
quality={40}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Lock scroll when open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
// Check for Escape key
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setIsOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", handleEsc);
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
window.removeEventListener("keydown", handleEsc);
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!src) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="relative group cursor-zoom-in my-6 w-full flex justify-center rounded-lg"
|
||||
onClick={() => setIsOpen(true)}
|
||||
aria-label="Zoom image"
|
||||
>
|
||||
<span className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors z-10 flex items-center justify-center opacity-0 group-hover:opacity-100 rounded-lg">
|
||||
<ZoomIn className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</span>
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width as Width}
|
||||
height={height as Height}
|
||||
quality={85}
|
||||
className="w-full h-auto rounded-lg transition-transform duration-300 group-hover:scale-[1.01]"
|
||||
{...props}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-99999 flex items-center justify-center bg-black/90 backdrop-blur-md p-4 md:p-10 cursor-zoom-out"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
className="absolute top-4 right-4 z-50 p-2 text-white/70 hover:text-white bg-black/20 hover:bg-white/10 rounded-full transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Image Container */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="relative max-w-7xl w-full h-full flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative w-full h-full flex items-center justify-center" onClick={() => setIsOpen(false)}>
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="object-contain max-h-[90vh] w-auto h-auto rounded-md shadow-2xl"
|
||||
quality={95}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Caption */}
|
||||
{alt && alt !== "alt" && (
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-black/60 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-md border border-white/10"
|
||||
>
|
||||
{alt}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</motion.div>
|
||||
</Portal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Portal = ({ children }: { children: React.ReactNode }) => {
|
||||
if (typeof window === "undefined") return null;
|
||||
return createPortal(children, document.body);
|
||||
};
|
||||
|
||||
@@ -1,52 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import {
|
||||
Info,
|
||||
AlertTriangle,
|
||||
ShieldAlert,
|
||||
CheckCircle,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
type NoteProps = PropsWithChildren & {
|
||||
title?: string;
|
||||
type?: "note" | "danger" | "warning" | "success";
|
||||
};
|
||||
const noteVariants = cva(
|
||||
"relative w-full rounded-lg border border-l-4 p-4 mb-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
note: "bg-muted/30 border-border border-l-primary/50 text-foreground [&>svg]:text-primary",
|
||||
danger: "border-destructive/20 border-l-destructive/60 bg-destructive/5 text-destructive [&>svg]:text-destructive dark:border-destructive/30",
|
||||
warning: "border-orange-500/20 border-l-orange-500/60 bg-orange-500/5 text-orange-600 dark:text-orange-400 [&>svg]:text-orange-600 dark:[&>svg]:text-orange-400",
|
||||
success: "border-emerald-500/20 border-l-emerald-500/60 bg-emerald-500/5 text-emerald-600 dark:text-emerald-400 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "note",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const iconMap = {
|
||||
note: <Info size={16} className="text-blue-500" />,
|
||||
danger: <ShieldAlert size={16} className="text-red-500" />,
|
||||
warning: <AlertTriangle size={16} className="text-orange-500" />,
|
||||
success: <CheckCircle size={16} className="text-green-500" />,
|
||||
note: Info,
|
||||
danger: ShieldAlert,
|
||||
warning: AlertTriangle,
|
||||
success: CheckCircle2,
|
||||
};
|
||||
|
||||
interface NoteProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof noteVariants> {
|
||||
title?: string;
|
||||
type?: "note" | "danger" | "warning" | "success";
|
||||
}
|
||||
|
||||
export default function Note({
|
||||
children,
|
||||
className,
|
||||
title = "Note",
|
||||
type = "note",
|
||||
children,
|
||||
...props
|
||||
}: NoteProps) {
|
||||
const noteClassNames = clsx({
|
||||
"dark:bg-stone-950/25 bg-stone-50": type === "note",
|
||||
"dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900":
|
||||
type === "danger",
|
||||
"bg-orange-50 border-orange-200 dark:border-orange-900 dark:bg-orange-900/50":
|
||||
type === "warning",
|
||||
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
|
||||
type === "success",
|
||||
});
|
||||
const Icon = iconMap[type] || Info;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border rounded-md px-5 pb-0.5 mt-5 mb-7 text-sm tracking-wide",
|
||||
noteClassNames
|
||||
)}
|
||||
className={cn(noteVariants({ variant: type }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2 font-bold -mb-2.5 pt-6">
|
||||
{iconMap[type]}
|
||||
<span className="text-base">{title}:</span>
|
||||
<Icon className="h-5 w-5" />
|
||||
<div className="pl-8">
|
||||
<h5 className="mb-1 font-medium leading-none tracking-tight">
|
||||
{title}
|
||||
</h5>
|
||||
<div className="text-sm [&_p]:leading-relaxed opacity-90">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SiSwift,
|
||||
SiKotlin,
|
||||
SiHtml5,
|
||||
SiCss3,
|
||||
SiCss,
|
||||
SiSass,
|
||||
SiPostgresql,
|
||||
SiGraphql,
|
||||
@@ -68,7 +68,7 @@ const LanguageIcon = ({ lang }: { lang: string }) => {
|
||||
js: <SiJavascript {...iconProps} />,
|
||||
javascript: <SiJavascript {...iconProps} />,
|
||||
html: <SiHtml5 {...iconProps} />,
|
||||
css: <SiCss3 {...iconProps} />,
|
||||
css: <SiCss {...iconProps} />,
|
||||
scss: <SiSass {...iconProps} />,
|
||||
sass: <SiSass {...iconProps} />,
|
||||
};
|
||||
|
||||
@@ -12,25 +12,29 @@ function Release({ version, title, date, children }: ReleaseProps) {
|
||||
|
||||
return (
|
||||
<div className="mb-16 group">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="bg-primary/10 text-primary border-2 border-primary/20 rounded-full px-4 py-1.5 text-base font-medium">
|
||||
v{version}
|
||||
</div>
|
||||
{date && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<div className="flex items-center gap-3 mt-6 mb-2">
|
||||
<div
|
||||
id={version}
|
||||
className="inline-flex items-center rounded-full border border-primary/20 bg-primary/10 px-3 py-1 text-sm font-semibold text-primary transition-colors hover:bg-primary/15 scroll-m-20 backdrop-blur-sm"
|
||||
>
|
||||
v{version}
|
||||
</div>
|
||||
{date && (
|
||||
<div className="flex items-center gap-3 text-sm font-medium text-muted-foreground">
|
||||
<span className="h-1 w-1 rounded-full bg-muted-foreground/30"></span>
|
||||
<time dateTime={date}>
|
||||
{new Date(date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-foreground/90 mb-3">
|
||||
{title}
|
||||
</h2>
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground/90 mb-6 mt-0!">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ const Tooltip: React.FC<TooltipProps> = ({ text, tip }) => {
|
||||
{text}
|
||||
</span>
|
||||
{visible && (
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-64 bg-popover text-popover-foreground text-sm p-3 rounded-md shadow-lg border border-border/50 break-words text-left z-50">
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-64 bg-popover text-popover-foreground text-sm p-3 rounded-md shadow-lg border border-border/50 wrap-break-word text-left z-50">
|
||||
{tip}
|
||||
<span className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-popover rotate-45 border-b border-r border-border/50 -z-10" />
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user