diff --git a/app/components/Dialog.tsx b/app/components/Dialog.tsx index bff04c5..91e2203 100644 --- a/app/components/Dialog.tsx +++ b/app/components/Dialog.tsx @@ -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; return ( <> - {cloneElement(button, triggerProps)} + {cloneElement(button, buttonTriggerProps)} {state.isOpen && ( {cloneElement(panel, { @@ -114,12 +115,12 @@ function Panel(props: DialogPanelProps) {
{children}
{variant === "unactionable" ? ( - + ) : ( <> - + - +
{props.description && (
{ - state: ToastState; -} - -function Toast({ state, ...props }: ToastProps) { - const ref = useRef(null); - const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(props, state, ref); - - return ( -
-
-
{props.toast.content}
-
- -
- ); -} - -interface ToastRegionProps extends AriaToastRegionProps { - state: ToastState; -} - -function ToastRegion({ state, ...props }: ToastRegionProps) { - const ref = useRef(null); - const { regionProps } = useToastRegion(props, state, ref); - - return ( -
- {state.visibleToasts.map((toast) => ( - - ))} -
- ); -} - -export interface ToastProviderProps extends AriaToastRegionProps { - queue: ToastQueue; -} - -export default function ToastProvider({ queue, ...props }: ToastProviderProps) { - const state = useToastQueue(queue); - - return <>{state.visibleToasts.length > 0 && }; -} diff --git a/app/components/button.tsx b/app/components/button.tsx index ef9ecce..9bad648 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -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 { variant?: "heavy" | "light" | "danger" | "ghost"; - className?: string; - children?: React.ReactNode; - "aria-label"?: string; - ref?: React.RefObject; } -export default function Button({ variant = "light", ...props }: ButtonProps) { - // In case the button is used as a trigger ref - const ref = props.ref ?? useRef(null); - const { buttonProps } = useButton(props, ref); - +export default function Button({ variant = "light", className, ...props }: ButtonProps) { return ( - + /> ); } diff --git a/app/components/page-error.tsx b/app/components/page-error.tsx index 265f75e..2b3486c 100644 --- a/app/components/page-error.tsx +++ b/app/components/page-error.tsx @@ -27,8 +27,8 @@ export default function PageError({ error, page }: PageErrorProps) { diff --git a/app/routes/dns/components/manage-domains.tsx b/app/routes/dns/components/manage-domains.tsx index 15352b0..2c2c9c7 100644 --- a/app/routes/dns/components/manage-domains.tsx +++ b/app/routes/dns/components/manage-domains.tsx @@ -170,7 +170,7 @@ function Domain({ domain, id, isDragging, isDisabled }: DomainProps) { + Rename Tailnet diff --git a/app/routes/dns/components/toggle-magic.tsx b/app/routes/dns/components/toggle-magic.tsx index c7fe3e4..7699872 100644 --- a/app/routes/dns/components/toggle-magic.tsx +++ b/app/routes/dns/components/toggle-magic.tsx @@ -11,7 +11,7 @@ interface Props { export default function Modal({ isEnabled, isDisabled }: Props) { return ( - + {isEnabled ? "Disable" : "Enable"} Magic DNS diff --git a/app/routes/home.tsx b/app/routes/home.tsx index d4fe53a..178eb5f 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -164,7 +164,7 @@ export default function Home({ loaderData }: Route.ComponentProps) { +
{createdKey ? ( @@ -100,7 +100,7 @@ export default function AddAuthKey({ {createdKey} + Add domain diff --git a/app/routes/settings/restrictions/dialogs/add-group.tsx b/app/routes/settings/restrictions/dialogs/add-group.tsx index 78985b2..5da5a6b 100644 --- a/app/routes/settings/restrictions/dialogs/add-group.tsx +++ b/app/routes/settings/restrictions/dialogs/add-group.tsx @@ -27,7 +27,7 @@ export default function AddGroup({ groups, isDisabled }: AddGroupProps) { return ( - + Add group diff --git a/app/routes/settings/restrictions/dialogs/add-user.tsx b/app/routes/settings/restrictions/dialogs/add-user.tsx index f2ac2f1..8da3336 100644 --- a/app/routes/settings/restrictions/dialogs/add-user.tsx +++ b/app/routes/settings/restrictions/dialogs/add-user.tsx @@ -27,7 +27,7 @@ export default function AddUser({ users, isDisabled }: AddUserProps) { return ( - + Add user diff --git a/app/routes/settings/restrictions/table.tsx b/app/routes/settings/restrictions/table.tsx index b13775b..481555d 100644 --- a/app/routes/settings/restrictions/table.tsx +++ b/app/routes/settings/restrictions/table.tsx @@ -37,7 +37,7 @@ export default function RestrictionTable({ children, type, values, isDisabled }: + Create a Headscale user diff --git a/app/tailwind.css b/app/tailwind.css index 0a04c80..1e174a6 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -78,6 +78,106 @@ height: 100% !important; } +.toast-viewport { + position: fixed; + z-index: 50; + bottom: 2rem; + right: 1rem; + left: auto; + top: auto; + width: 20rem; +} + +.toast-root { + --gap: 0.75rem; + --peek: 0.75rem; + --scale: calc(max(0, 1 - (var(--toast-index) * 0.1))); + --shrink: calc(1 - var(--scale)); + --height: var(--toast-frontmost-height, var(--toast-height)); + --offset-y: calc( + var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + + var(--toast-swipe-movement-y) + ); + + position: absolute; + right: 0; + bottom: 0; + left: auto; + width: 100%; + height: var(--height); + z-index: calc(1000 - var(--toast-index)); + transform-origin: bottom center; + user-select: none; + + transform: translateX(var(--toast-swipe-movement-x)) + translateY( + calc( + var(--toast-swipe-movement-y) - (var(--toast-index) * var(--peek)) - + (var(--shrink) * var(--height)) + ) + ) + scale(var(--scale)); + + transition: + transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.5s, + height 0.15s; + + &[data-expanded] { + transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y)); + height: var(--toast-height); + } + + &[data-starting-style] { + transform: translateY(150%); + } + + &[data-ending-style] { + opacity: 0; + transform: translateY(150%); + + &[data-swipe-direction="up"] { + transform: translateY(calc(var(--toast-swipe-movement-y) - 150%)); + } + &[data-swipe-direction="down"] { + transform: translateY(calc(var(--toast-swipe-movement-y) + 150%)); + } + &[data-swipe-direction="left"] { + transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y)); + } + &[data-swipe-direction="right"] { + transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y)); + } + } + + &[data-limited] { + opacity: 0; + } + + /* Invisible hit area to bridge hover gap between toasts */ + &::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: calc(var(--gap) + 1px); + } +} + +.toast-content { + overflow: hidden; + transition: opacity 0.25s; + + &[data-behind] { + opacity: 0; + } + + &[data-expanded] { + opacity: 1; + } +} + /* Weirdest class name characters but ok */ .cm-mergeView .ͼ1 .cm-scroller, .cm-mergeView .ͼ1 { diff --git a/app/utils/toast-provider.tsx b/app/utils/toast-provider.tsx new file mode 100644 index 0000000..82cce41 --- /dev/null +++ b/app/utils/toast-provider.tsx @@ -0,0 +1,49 @@ +import { Toast } from "@base-ui/react/toast"; +import { X } from "lucide-react"; + +import cn from "~/utils/cn"; +import { toastManager } from "~/utils/toast"; + +function ToastList() { + const { toasts } = Toast.useToastManager(); + return toasts.map((toast) => ( + + + {toast.description} + + + + + + )); +} + +export default function ToastProvider() { + return ( + + + + + + + + ); +} diff --git a/app/utils/toast.ts b/app/utils/toast.ts index 5509ac8..f939ba4 100644 --- a/app/utils/toast.ts +++ b/app/utils/toast.ts @@ -1,14 +1,7 @@ -import { ToastQueue } from '@react-stately/toast'; -import React from 'react'; +import { Toast } from "@base-ui/react/toast"; -const toastQueue = new ToastQueue({ - maxVisibleToasts: 7, -}); +export const toastManager = Toast.createToastManager(); -export function useToastQueue() { - return toastQueue; -} - -export default function toast(content: React.ReactNode, duration = 3000) { - return toastQueue.add(content, { timeout: duration }); +export default function toast(content: string, duration = 3000) { + return toastManager.add({ description: content, timeout: duration }); } diff --git a/package.json b/package.json index f0a1769..ad5eec7 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,7 @@ "@fontsource-variable/inter": "^5.2.8", "@iconify/react": "^6.0.2", "@kubernetes/client-node": "^1.4.0", - "@react-aria/toast": "3.0.10", "@react-router/node": "^7.13.1", - "@react-stately/toast": "3.1.3", "@readme/openapi-parser": "^5.5.0", "@uiw/codemirror-theme-github": "4.25.1", "@uiw/codemirror-theme-xcode": "4.25.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfbc961..397ca71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,15 +37,9 @@ importers: '@kubernetes/client-node': specifier: ^1.4.0 version: 1.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@react-aria/toast': - specifier: 3.0.10 - version: 3.0.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-router/node': specifier: ^7.13.1 version: 7.13.1(react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@react-stately/toast': - specifier: 3.1.3 - version: 3.1.3(react@19.2.4) '@readme/openapi-parser': specifier: ^5.5.0 version: 5.5.0(openapi-types@12.1.3)