UNPKG

raft-ui

Version:

React UI components for Raft.

469 lines (462 loc) • 15.8 kB
import { cn } from "./cn.mjs"; import { Dialog } from "@base-ui/react/dialog"; import { X } from "lucide-react"; import { tv } from "tailwind-variants/lite"; import { createContext, use } from "react"; import { Button } from "@base-ui/react/button"; import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime"; //#region src/lib/tv.ts const tv$1 = ((options) => { const recipe = tv(options); const wrappedRecipe = ((props) => { const result = recipe(props); if (result == null || typeof result === "string") return cn(result); return Object.fromEntries(Object.entries(result).map(([slotName, slot]) => [slotName, (slotProps) => cn(slot(slotProps))])); }); return Object.assign(wrappedRecipe, recipe); }); //#endregion //#region src/lib/theme/theme-context.ts const ThemeStateContext = createContext({ mode: "light", resolvedMode: "light", setMode: () => void 0 }); //#endregion //#region src/lib/theme/use-theme.ts /** Read and update the colour mode. */ function useTheme() { return use(ThemeStateContext); } //#endregion //#region src/lib/theme/theme-scope.ts /** * Carry the provider's resolved mode (`light` / `dark`) onto content that * renders outside its subtree (a portal), so a dark island's popup stays dark. */ function resolveThemeScopeProps({ className, ...props }, { resolvedMode }) { const resolvedClassName = typeof className === "function" ? (state) => cn(className(state), resolvedMode) : typeof className === "string" ? cn(className, resolvedMode) : className ?? resolvedMode; return { ...props, className: resolvedClassName }; } function useThemeScopeProps(props) { return resolveThemeScopeProps(props, useTheme()); } //#endregion //#region src/lib/default-native-button.ts function defaultNativeButton(render) { if (!render || typeof render !== "object" || !("type" in render)) return; const element = render; if (typeof element.type === "string") return element.type === "button"; } //#endregion //#region src/components/spinner/spinner.tsx const SPINNER_STYLE = ` @keyframes raft-spinner-pixel { 0%, 24% { opacity: 1; } 25%, 100% { opacity: 0.2; } } [data-slot="spinner"] [data-spinner-pixel] { animation: raft-spinner-pixel 1s steps(1) infinite; } [data-slot="spinner"] [data-spinner-pixel]:nth-child(2) { animation-delay: 0.25s; } [data-slot="spinner"] [data-spinner-pixel]:nth-child(4) { animation-delay: 0.5s; } [data-slot="spinner"] [data-spinner-pixel]:nth-child(3) { animation-delay: 0.75s; } @media (prefers-reduced-motion: reduce) { [data-slot="spinner"] [data-spinner-pixel] { animation: none; } [data-slot="spinner"] [data-spinner-pixel]:first-child { opacity: 1; } } `; const spinner = tv$1({ slots: { root: [ "relative inline-flex shrink-0 items-center justify-center", "leading-none text-current", "[--spinner-pixel:5px] [--spinner-gap:2px]" ], ring: ["shrink-0", "grid size-auto grid-cols-[repeat(2,var(--spinner-pixel))] gap-(--spinner-gap)"], pixel: ["block size-(--spinner-pixel) rounded-[1px] bg-current opacity-20"] }, variants: { size: { xs: { root: ["[--spinner-pixel:4px] [--spinner-gap:1.5px]"] }, sm: {}, md: { root: ["[--spinner-pixel:6px] [--spinner-gap:2px]"] }, lg: { root: ["[--spinner-pixel:8px] [--spinner-gap:3px]"] } }, variant: { default: {}, inverse: { root: ["text-foreground-inverse"] } } }, defaultVariants: { size: "sm" } }); const spinnerStyle = /* @__PURE__ */ jsx("style", { href: "raft-ui-spinner", precedence: "default", children: SPINNER_STYLE }); function Spinner({ size = "sm", variant = "default", className, render, ...props }) { const { root, ring, pixel } = spinner({ size, variant }); const children = /* @__PURE__ */ jsxs(Fragment$1, { children: [spinnerStyle, /* @__PURE__ */ jsxs("span", { "data-spinner-grid": "", "aria-hidden": "true", className: ring(), children: [ /* @__PURE__ */ jsx("span", { "data-spinner-pixel": "", className: pixel() }), /* @__PURE__ */ jsx("span", { "data-spinner-pixel": "", className: pixel() }), /* @__PURE__ */ jsx("span", { "data-spinner-pixel": "", className: pixel() }), /* @__PURE__ */ jsx("span", { "data-spinner-pixel": "", className: pixel() }) ] })] }); const defaultProps = { role: "status", "aria-label": "Loading", className: root({ className }), children }; return useRender({ defaultTagName: "span", render, props: mergeProps(defaultProps, props), state: { slot: "spinner", size } }); } //#endregion //#region src/components/button/button.tsx const sticker = (fill) => [`border-sticker-ink ${fill} text-sticker-ink ring-[1.5px] ring-sticker-edge`, "hover:press-overlay-sticker active:press-overlay-sticker"]; const outline = (ink) => [`border-line-strong bg-layer-card ${ink}`, "hover:press-overlay active:press-overlay"]; const textMd = [ "h-8 gap-2 rounded-control px-3 py-0 text-footnote", "has-data-[icon=inline-start]:pl-2.5 has-data-[icon=inline-end]:pr-2.5", "[&_svg:not([class*='size-'])]:size-4 **:data-icon:size-[1em]", "after:absolute after:inset-x-0 after:-inset-y-1.5" ]; const iconMd = ["size-8 rounded-control p-0 [&_svg:not([class*='size-'])]:size-[18px]", "after:absolute after:-inset-1.5"]; const buttonVariants = tv$1({ slots: { root: [ "relative inline-flex shrink-0 items-center justify-center", "whitespace-nowrap select-none", "rounded-none border-[1.5px] font-sans font-bold shadow-none", "transition-colors duration-100", "outline-[1px] outline-offset-2 outline-transparent focus-visible:outline-transparent", "focus-visible:shadow-raft-focus", "data-[loading=true]:cursor-wait data-[loading=true]:opacity-100", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "disabled:pointer-events-none aria-disabled:cursor-not-allowed", "disabled:transform-none aria-disabled:transform-none", "aria-disabled:hover:translate-none", "aria-disabled:active:translate-none aria-disabled:active:scale-none", "disabled:opacity-100 aria-disabled:opacity-100", "disabled:border-line-muted disabled:bg-fill-muted disabled:bg-none disabled:text-foreground-disabled disabled:ring-0", "aria-disabled:border-line-muted aria-disabled:bg-fill-muted aria-disabled:bg-none aria-disabled:text-foreground-disabled aria-disabled:ring-0" ], content: "relative z-10 inline-flex items-center justify-center gap-[inherit] data-[loading=true]:invisible", loadingIndicator: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center" }, variants: { variant: { primary: { root: sticker("bg-accent-400") }, accent: { root: sticker("bg-accent-400") }, warning: { root: sticker("bg-warning") }, danger: { root: sticker("bg-danger") }, default: { root: outline("text-foreground") }, outline: { root: outline("text-foreground") }, information: { root: outline("text-foreground") }, muted: { root: outline("text-foreground") }, success: { root: outline("text-foreground") }, "danger-secondary": { root: outline("text-danger-strong") }, "danger-outline": { root: outline("text-danger-strong") }, ghost: { root: ["border-transparent bg-transparent text-foreground", "hover:bg-fill-pressed active:bg-fill-pressed"] }, link: { root: [ "border-transparent bg-transparent text-foreground", "underline decoration-[1.5px] underline-offset-[3px]", "hover:decoration-accent-500 aria-disabled:hover:no-underline" ] } }, size: { xs: { root: textMd }, sm: { root: textMd }, md: { root: textMd }, lg: { root: [ "h-11 gap-2 rounded-control px-4 py-0 text-subhead", "has-data-[icon=inline-start]:pl-3.5 has-data-[icon=inline-end]:pr-3.5", "[&_svg:not([class*='size-'])]:size-[18px] **:data-icon:size-[1em]" ] }, xl: { root: ["h-13 gap-2 rounded-control px-5 py-0 text-body", "[&_svg:not([class*='size-'])]:size-[18px] **:data-icon:size-[1em]"] }, inline: { root: ["h-auto gap-1 border-0 p-0 align-baseline leading-[inherit] whitespace-normal", "**:data-icon:size-[1em]"] }, "icon-xs": { root: iconMd }, "icon-sm": { root: iconMd }, "icon-md": { root: iconMd }, "icon-lg": { root: "size-11 rounded-control p-0 [&_svg:not([class*='size-'])]:size-[22px]" } } }, defaultVariants: { variant: "default", size: "md" }, compoundVariants: [{ variant: "link", class: { root: "px-0" } }] }); function Button$1({ variant, size, loading = false, loadingLabel = "Loading", disabled, className, children, render, nativeButton, ...props }) { const { root, content, loadingIndicator } = buttonVariants({ variant, size }); return /* @__PURE__ */ jsxs(Button, { ...props, render, nativeButton: nativeButton ?? defaultNativeButton(render), "data-slot": "button", className: root({ className }), "data-loading": loading ? "true" : void 0, disabled: disabled || loading, ...loading ? { "aria-disabled": true } : {}, children: [loading ? /* @__PURE__ */ jsx("span", { "data-slot": "button-loading-indicator", className: loadingIndicator(), children: /* @__PURE__ */ jsx(Spinner, { "aria-label": loadingLabel }) }) : null, /* @__PURE__ */ jsx("span", { "data-slot": "button-content", "data-loading": loading ? "true" : void 0, className: content(), children })] }); } //#endregion //#region src/components/dialog/dialog.tsx const dialog = tv$1({ slots: { overlay: ["fixed inset-0 z-50 bg-layer-backdrop"], content: [ "fixed top-1/2 left-1/2 z-50 flex w-[calc(100%-2rem)] -translate-1/2 flex-col", "max-h-[calc(100dvh-2rem)] outline-none", "overflow-clip rounded-dialog bg-layer-popover text-foreground", "shadow-raft-float ring-1 ring-line-seam" ], header: ["grid shrink-0 grid-cols-[minmax(0,1fr)_max-content] items-center min-w-0", "gap-2 px-5 pt-5"], body: [ "min-w-0 flex-1 overflow-y-auto", "px-5 pt-1.5 pb-5 first:pt-5", "font-sans text-subhead text-foreground-muted" ], footer: ["flex shrink-0 items-center justify-end", "gap-2 px-5 pb-5 first:pt-5 [[data-slot=dialog-header]+&]:pt-5"], title: ["m-0 min-w-0 text-foreground", "font-sans text-headline font-bold"], description: ["m-0", "font-sans text-subhead text-foreground-muted"] }, variants: { size: { sm: { content: ["max-w-sm"] }, md: { content: ["max-w-md"] }, lg: { content: ["max-w-lg"] }, wide: { content: ["max-w-4xl h-[min(760px,calc(100dvh-2rem))]"] } }, bar: { false: {}, true: { header: ["flex h-14 shrink-0 items-center gap-2 border-b px-5 py-0", "border-line-seam pt-0 [&>[data-slot=dialog-close]]:my-0"], body: ["flex min-h-0 flex-1 overflow-hidden p-0", "px-0 pt-0 pb-0 first:pt-0 text-foreground"], footer: ["shrink-0 border-t px-5 py-3", "border-line-seam pt-3 pb-3 first:pt-3 [[data-slot=dialog-header]+&]:pt-3"] } }, layer: { 0: { overlay: [], content: [] }, 1: { overlay: ["z-60"], content: ["z-60"] } } }, defaultVariants: { size: "md", bar: false } }); function Dialog$1(props) { return /* @__PURE__ */ jsx(Dialog.Root, { "data-slot": "dialog", ...props }); } function DialogTrigger(props) { return /* @__PURE__ */ jsx(Dialog.Trigger, { "data-slot": "dialog-trigger", ...props }); } function DialogPortal(props) { return /* @__PURE__ */ jsx(Dialog.Portal, { "data-slot": "dialog-portal", ...useThemeScopeProps(props) }); } function DialogOverlay({ className, layer, ...props }) { const { overlay } = dialog({ layer }); return /* @__PURE__ */ jsx(Dialog.Backdrop, { "data-slot": "dialog-overlay", className: overlay({ className }), ...props }); } function DialogContent({ className, children, portalProps, overlay, size, layer, ...props }) { const { content } = dialog({ size, layer }); const overlayProps = typeof overlay === "object" ? overlay : {}; return /* @__PURE__ */ jsxs(DialogPortal, { ...portalProps, children: [overlay === false ? null : /* @__PURE__ */ jsx(DialogOverlay, { layer, ...overlayProps }), /* @__PURE__ */ jsx(Dialog.Popup, { "data-slot": "dialog-content", className: content({ className }), ...props, children })] }); } function DialogClose({ className, variant, size, children, render, "aria-label": ariaLabel, ...props }) { if (render) return /* @__PURE__ */ jsx(Dialog.Close, { "data-slot": "dialog-close", render, "aria-label": ariaLabel, ...props, children }); return /* @__PURE__ */ jsx(Dialog.Close, { "data-slot": "dialog-close", render: /* @__PURE__ */ jsx(Button$1, { variant: variant ?? "ghost", size: size ?? "icon-md", "aria-label": ariaLabel ?? "Close", className: cn("-my-1.5 -mr-1.5", className) }), ...props, children: children ?? /* @__PURE__ */ jsx(X, { className: "size-4.5" }) }); } function DialogHeader({ className, bar = false, ...props }) { const { header } = dialog({ bar }); return /* @__PURE__ */ jsx("div", { "data-slot": "dialog-header", className: header({ className }), ...props }); } function DialogBody({ className, bar = false, ...props }) { const { body } = dialog({ bar }); return /* @__PURE__ */ jsx("div", { "data-slot": "dialog-body", className: body({ className }), ...props }); } function DialogFooter({ className, bar = false, ...props }) { const { footer } = dialog({ bar }); return /* @__PURE__ */ jsx("div", { "data-slot": "dialog-footer", className: footer({ className }), ...props }); } function DialogTitle({ className, ...props }) { const { title } = dialog(); return /* @__PURE__ */ jsx(Dialog.Title, { "data-slot": "dialog-title", className: title({ className }), ...props }); } function DialogDescription({ className, ...props }) { const { description } = dialog(); return /* @__PURE__ */ jsx(Dialog.Description, { "data-slot": "dialog-description", className: description({ className }), ...props }); } const dialogSurface = tv$1({ slots: { root: [ "flex min-w-0 flex-col outline-none", "overflow-clip rounded-dialog bg-layer-popover text-foreground", "shadow-raft-float ring-1 ring-line-seam" ] } }); /** * The dialog's frame — the popover layer, the dialog radius, the seam ring * and the float shadow — for a centred modal whose lifecycle the app owns * (its own backdrop, Escape and focus handling). It neither positions nor * animates itself: the host centres it, and a centred modal has no motion. * DialogContent draws the same frame for a Base UI dialog. */ function DialogSurface({ className, render, ...props }) { const { root } = dialogSurface(); return useRender({ defaultTagName: "div", render, props: mergeProps({ "data-slot": "dialog-surface", className: root({ className }) }, props) }); } const dialogNav = tv$1({ slots: { root: ["flex shrink-0 flex-col overflow-y-auto", "w-52 gap-0.5 border-r border-line-seam p-2 max-md:hidden"] } }); /** The section list of a wide (`size="wide"`) dialog, inside a `bar` body. */ function DialogNav({ className, render, ...props }) { const { root } = dialogNav(); return useRender({ defaultTagName: "nav", render, props: mergeProps({ "data-slot": "dialog-nav", className: root({ className }) }, props) }); } //#endregion export { tv$1 as S, defaultNativeButton as _, DialogDescription as a, useTheme as b, DialogNav as c, DialogSurface as d, DialogTitle as f, Spinner as g, buttonVariants as h, DialogContent as i, DialogOverlay as l, Button$1 as m, DialogBody as n, DialogFooter as o, DialogTrigger as p, DialogClose as r, DialogHeader as s, Dialog$1 as t, DialogPortal as u, resolveThemeScopeProps as v, ThemeStateContext as x, useThemeScopeProps as y };