raft-ui
Version:
React UI components for Raft.
469 lines (462 loc) • 15.8 kB
JavaScript
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 };