mirror of
https://github.com/tale/headplane.git
synced 2026-08-22 10:46:38 +00:00
feat: replace button and toast provider with base-ui
This commit is contained in:
@@ -44,9 +44,10 @@ function Dialog(props: DialogProps) {
|
||||
|
||||
if (Array.isArray(props.children)) {
|
||||
const [button, panel] = props.children;
|
||||
const buttonTriggerProps = { onClick: triggerProps.onPress } as Record<string, unknown>;
|
||||
return (
|
||||
<>
|
||||
{cloneElement(button, triggerProps)}
|
||||
{cloneElement(button, buttonTriggerProps)}
|
||||
{state.isOpen && (
|
||||
<DModal state={state}>
|
||||
{cloneElement(panel, {
|
||||
@@ -114,12 +115,12 @@ function Panel(props: DialogPanelProps) {
|
||||
<div className="flex flex-col gap-4">{children}</div>
|
||||
<div className="mt-5 flex justify-end gap-3">
|
||||
{variant === "unactionable" ? (
|
||||
<Button onPress={close}>Close</Button>
|
||||
<Button onClick={close}>Close</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button onPress={close}>Cancel</Button>
|
||||
<Button onClick={close}>Cancel</Button>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
disabled={isDisabled}
|
||||
type="submit"
|
||||
variant={variant === "destructive" ? "danger" : "heavy"}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Minus, Plus } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { type AriaNumberFieldProps, useId, useLocale, useNumberField } from "react-aria";
|
||||
import { type AriaNumberFieldProps, useButton, useId, useLocale, useNumberField } from "react-aria";
|
||||
import { useNumberFieldState } from "react-stately";
|
||||
|
||||
import Button from "~/components/button";
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
export interface InputProps extends AriaNumberFieldProps {
|
||||
@@ -30,6 +29,11 @@ export default function NumberInput(props: InputProps) {
|
||||
validationErrors,
|
||||
} = useNumberField(props, state, ref);
|
||||
|
||||
const decrRef = useRef<HTMLButtonElement | null>(null);
|
||||
const incrRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { buttonProps: decrProps } = useButton(decrementButtonProps, decrRef);
|
||||
const { buttonProps: incrProps } = useButton(incrementButtonProps, incrRef);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<label
|
||||
@@ -57,20 +61,22 @@ export default function NumberInput(props: InputProps) {
|
||||
className="w-full rounded-l-md bg-transparent py-2 pl-3 focus:outline-hidden"
|
||||
/>
|
||||
<input type="hidden" name={name} value={state.numberValue} />
|
||||
<Button
|
||||
{...decrementButtonProps}
|
||||
<button
|
||||
{...decrProps}
|
||||
ref={decrRef}
|
||||
aria-label="Decrement"
|
||||
className="h-7.5 w-7.5 rounded-lg p-1"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
{...incrementButtonProps}
|
||||
</button>
|
||||
<button
|
||||
{...incrProps}
|
||||
ref={incrRef}
|
||||
aria-label="Increment"
|
||||
className="h-7.5 w-7.5 rounded-lg p-1"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
{props.description && (
|
||||
<div
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { AriaToastProps, AriaToastRegionProps, useToast, useToastRegion } from "@react-aria/toast";
|
||||
import { ToastQueue, ToastState, useToastQueue } from "@react-stately/toast";
|
||||
import { X } from "lucide-react";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import Button from "~/components/button";
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
interface ToastProps extends AriaToastProps<React.ReactNode> {
|
||||
state: ToastState<React.ReactNode>;
|
||||
}
|
||||
|
||||
function Toast({ state, ...props }: ToastProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(props, state, ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...toastProps}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-x-3 pl-4 pr-3",
|
||||
"text-white shadow-overlay rounded-lg py-3",
|
||||
"bg-mist-900 dark:bg-mist-950",
|
||||
)}
|
||||
>
|
||||
<div {...contentProps} className="flex flex-col gap-2">
|
||||
<div {...titleProps}>{props.toast.content}</div>
|
||||
</div>
|
||||
<Button
|
||||
{...closeButtonProps}
|
||||
aria-label="Close"
|
||||
className={cn(
|
||||
"rounded-full p-1",
|
||||
"bg-transparent hover:bg-mist-700",
|
||||
"dark:bg-transparent dark:hover:bg-mist-800",
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<React.ReactNode>;
|
||||
}
|
||||
|
||||
function ToastRegion({ state, ...props }: ToastRegionProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const { regionProps } = useToastRegion(props, state, ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...regionProps}
|
||||
ref={ref}
|
||||
className={cn("fixed bottom-20 right-4", "flex flex-col gap-4")}
|
||||
>
|
||||
{state.visibleToasts.map((toast) => (
|
||||
<Toast key={toast.key} toast={toast} state={state} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ToastProviderProps extends AriaToastRegionProps {
|
||||
queue: ToastQueue<React.ReactNode>;
|
||||
}
|
||||
|
||||
export default function ToastProvider({ queue, ...props }: ToastProviderProps) {
|
||||
const state = useToastQueue(queue);
|
||||
|
||||
return <>{state.visibleToasts.length > 0 && <ToastRegion {...props} state={state} />}</>;
|
||||
}
|
||||
@@ -1,32 +1,22 @@
|
||||
import React, { useRef } from "react";
|
||||
import { type AriaButtonOptions, useButton } from "react-aria";
|
||||
import { Button as BaseButton } from "@base-ui/react/button";
|
||||
import React from "react";
|
||||
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
export interface ButtonProps extends AriaButtonOptions<"button"> {
|
||||
export interface ButtonProps extends React.ComponentProps<typeof BaseButton> {
|
||||
variant?: "heavy" | "light" | "danger" | "ghost";
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
"aria-label"?: string;
|
||||
ref?: React.RefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
|
||||
export default function Button({ variant = "light", ...props }: ButtonProps) {
|
||||
// In case the button is used as a trigger ref
|
||||
const ref = props.ref ?? useRef<HTMLButtonElement | null>(null);
|
||||
const { buttonProps } = useButton(props, ref);
|
||||
|
||||
export default function Button({ variant = "light", className, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...buttonProps}
|
||||
aria-label={props["aria-label"]}
|
||||
<BaseButton
|
||||
{...props}
|
||||
className={cn(
|
||||
"inline-flex w-fit items-center justify-center gap-2 rounded-md px-3.5 py-2 text-sm",
|
||||
"transition-colors duration-100",
|
||||
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
|
||||
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
|
||||
props.isDisabled && "pointer-events-none opacity-50",
|
||||
props.disabled && "pointer-events-none opacity-50",
|
||||
...(variant === "heavy"
|
||||
? [
|
||||
"bg-indigo-500 font-semibold text-white",
|
||||
@@ -50,10 +40,8 @@ export default function Button({ variant = "light", ...props }: ButtonProps) {
|
||||
"dark:border-mist-700 dark:bg-mist-800/50",
|
||||
"dark:hover:bg-mist-700/50",
|
||||
]),
|
||||
props.className,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export default function PageError({ error, page }: PageErrorProps) {
|
||||
<Button
|
||||
className="mt-6"
|
||||
variant="light"
|
||||
onPress={() => revalidate()}
|
||||
isDisabled={state === "loading"}
|
||||
onClick={() => revalidate()}
|
||||
disabled={state === "loading"}
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn("mr-2 inline-block h-4 w-4", state === "loading" && "animate-spin")}
|
||||
|
||||
Reference in New Issue
Block a user