quality / commitlint (push) Skipped
CD / update-wiki (push) Failing after 7s
quality / changes (push) Successful in 4s
quality / docker-check (push) Skipped
quality / web (push) Failing after 38s
quality / api (push) Successful in 49s
CD / quality (push) Failing after 1m36s
CD / publish (push) Skipped
175 lines
7.3 KiB
TypeScript
175 lines
7.3 KiB
TypeScript
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { cn } from "@cdnmanager/ui/lib/utils"
|
|
|
|
/**
|
|
* CSS variable architecture for FramePanel theming:
|
|
*
|
|
* The Frame parent sets --frame-panel-bg and --frame-panel-border-color.
|
|
* FramePanel consumes them directly via bg-(--frame-panel-bg) and
|
|
* border-(--frame-panel-border-color). This means:
|
|
*
|
|
* - variant="inverse" overrides those vars on Frame → all panels pick it up
|
|
* - <FramePanel className="bg-blue-50"> adds a direct utility on the element
|
|
* which wins over bg-(--frame-panel-bg) by Tailwind source order — no
|
|
* :not() or !important needed
|
|
*/
|
|
const frameVariants = cva(
|
|
[
|
|
"relative flex flex-col bg-muted/50 gap-(--frame-gap) px-(--frame-px) py-(--frame-py) rounded-(--frame-radius)",
|
|
"(--radius-xl)] [--frame-radius:var(--radius-xl)]",
|
|
"(--radius-none)] (--radius-2xl)] (--radius-lg)] (--radius-none)]",
|
|
"[--frame-gap:--spacing(0.75)] [--frame-px:--spacing(0.75)] [--frame-py:--spacing(0.75)] [--frame-panel-header-gap:0rem] [--frame-panel-footer-gap:--spacing(1)]",
|
|
"[--frame-panel-px-adjust:0px] [--frame-panel-py-adjust:0px] [--frame-panel-header-px-adjust:0px] [--frame-panel-header-py-adjust:0px] [--frame-panel-footer-px-adjust:0px] [--frame-panel-footer-py-adjust:0px]",
|
|
"[--frame-panel-px:calc(var(--frame-panel-px-base)_+_var(--frame-panel-px-adjust))] [--frame-panel-py:calc(var(--frame-panel-py-base)_+_var(--frame-panel-py-adjust))] [--frame-panel-header-px:calc(var(--frame-panel-header-px-base)_+_var(--frame-panel-header-px-adjust))] [--frame-panel-header-py:calc(var(--frame-panel-header-py-base)_+_var(--frame-panel-header-py-adjust))] [--frame-panel-footer-px:calc(var(--frame-panel-footer-px-base)_+_var(--frame-panel-footer-px-adjust))] [--frame-panel-footer-py:calc(var(--frame-panel-footer-py-base)_+_var(--frame-panel-footer-py-adjust))]",
|
|
"(1)] (1)] (1.25)] (1.5)] (1.5)] (0.5)] (1)] (1)]",
|
|
// Default panel token values — overridden per-variant below
|
|
"[--frame-panel-bg:var(--color-card)] [--frame-panel-border-color:var(--color-border)] [--frame-border-color:var(--color-border)]",
|
|
],
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "border border-[var(--frame-border-color)] bg-clip-padding",
|
|
inverse:
|
|
"[--frame-panel-bg:color-mix(in_oklch,var(--color-muted)_40%,transparent)] border border-[var(--frame-border-color)] bg-background bg-clip-padding",
|
|
ghost: "",
|
|
},
|
|
spacing: {
|
|
xs: "[--frame-panel-px-base:--spacing(2)] [--frame-panel-py-base:--spacing(2)] [--frame-panel-header-px-base:--spacing(2)] [--frame-panel-header-py-base:--spacing(1)] [--frame-panel-footer-px-base:--spacing(2)] [--frame-panel-footer-py-base:--spacing(1)] (3)] (1)] (3)] (3)]",
|
|
sm: "[--frame-panel-px-base:--spacing(3)] [--frame-panel-py-base:--spacing(3.5)] [--frame-panel-header-px-base:--spacing(3)] [--frame-panel-header-py-base:--spacing(2.5)] [--frame-panel-footer-px-base:--spacing(3)] [--frame-panel-footer-py-base:--spacing(2.5)] (2)] (2)] (2)]",
|
|
default:
|
|
"[--frame-panel-px-base:--spacing(4)] [--frame-panel-py-base:--spacing(4)] [--frame-panel-header-px-base:--spacing(4)] [--frame-panel-header-py-base:--spacing(3)] [--frame-panel-footer-px-base:--spacing(4)] [--frame-panel-footer-py-base:--spacing(3)] (2)] (2)] (2)]",
|
|
lg: "[--frame-panel-px-base:--spacing(5)] [--frame-panel-py-base:--spacing(5)] [--frame-panel-header-px-base:--spacing(5)] [--frame-panel-header-py-base:--spacing(4)] [--frame-panel-footer-px-base:--spacing(5)] [--frame-panel-footer-py-base:--spacing(4)] (2)] (2)] (2)]",
|
|
},
|
|
stacked: {
|
|
true: [
|
|
"gap-0 *:has-[+[data-slot=frame-panel]]:rounded-b-none",
|
|
"*:has-[+[data-slot=frame-panel]]:before:hidden",
|
|
"*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:rounded-t-none",
|
|
"*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:border-t-0",
|
|
],
|
|
false: [
|
|
"data-[spacing=sm]:*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:mt-0.5",
|
|
"data-[spacing=default]:*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:mt-1",
|
|
"data-[spacing=lg]:*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:mt-2",
|
|
],
|
|
},
|
|
dense: {
|
|
// Positional rules must stay as parent selectors — cannot be expressed via CSS vars
|
|
true: "p-0 gap-0 border-[var(--frame-border-color)] [&_[data-slot=frame-panel]]:-mx-px [&_[data-slot=frame-panel]]:before:hidden [&_[data-slot=frame-panel]:last-child]:-mb-px [&:not(:has([data-slot=frame-panel-header]))_[data-slot=frame-panel]:is(:first-child)]:-mt-px",
|
|
false: "",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
spacing: "default",
|
|
stacked: false,
|
|
dense: false,
|
|
},
|
|
}
|
|
)
|
|
|
|
function Frame({
|
|
className,
|
|
variant,
|
|
spacing,
|
|
stacked,
|
|
dense,
|
|
...props
|
|
}: React.ComponentProps<"div"> & VariantProps<typeof frameVariants>) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
frameVariants({ variant, spacing, stacked, dense }),
|
|
className
|
|
)}
|
|
data-slot="frame"
|
|
data-spacing={spacing}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function FramePanel({
|
|
className,
|
|
fit,
|
|
...props
|
|
}: React.ComponentProps<"div"> & { fit?: boolean }) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
// bg-(--frame-panel-bg) and border-(--frame-panel-border-color) consume the
|
|
// CSS vars set by the Frame parent. Any explicit bg-* or border-* class passed
|
|
// via className overrides these by Tailwind source order - no ! needed.
|
|
"relative overflow-hidden rounded-(--frame-radius) border border-(--frame-panel-border-color) bg-(--frame-panel-bg) bg-clip-padding shadow-xs",
|
|
// `fit` sizes the panel to its content; otherwise it grows to fill the frame.
|
|
!fit && "grow",
|
|
"before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--frame-radius)-1px)] before:shadow-black/5",
|
|
"dark:bg-clip-border dark:before:shadow-white/5",
|
|
"px-(--frame-panel-px) py-(--frame-panel-py)",
|
|
className
|
|
)}
|
|
data-slot="frame-panel"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function FrameHeader({ className, ...props }: React.ComponentProps<"header">) {
|
|
return (
|
|
<header
|
|
className={cn(
|
|
"flex flex-col gap-(--frame-panel-header-gap) px-(--frame-panel-header-px) py-(--frame-panel-header-py)",
|
|
className
|
|
)}
|
|
data-slot="frame-panel-header"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function FrameTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
className={cn("text-sm font-semibold", className)}
|
|
data-slot="frame-panel-title"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function FrameDescription({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
className={cn("text-muted-foreground text-sm", className)}
|
|
data-slot="frame-panel-description"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function FrameFooter({ className, ...props }: React.ComponentProps<"footer">) {
|
|
return (
|
|
<footer
|
|
className={cn(
|
|
"flex flex-col gap-(--frame-panel-footer-gap) px-(--frame-panel-footer-px) py-(--frame-panel-footer-py)",
|
|
className
|
|
)}
|
|
data-slot="frame-panel-footer"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export {
|
|
Frame,
|
|
FramePanel,
|
|
FrameHeader,
|
|
FrameTitle,
|
|
FrameDescription,
|
|
FrameFooter,
|
|
frameVariants,
|
|
} |