feat: completely nuke headlessui and consolidate to aria
This commit is contained in:
parent
8205f2b99b
commit
a57e777a6b
@ -1,5 +1,6 @@
|
|||||||
import { ClipboardIcon } from '@heroicons/react/24/outline'
|
import { ClipboardIcon } from '@heroicons/react/24/outline'
|
||||||
import toast from 'react-hot-toast/headless'
|
|
||||||
|
import { toast } from './Toaster'
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable unicorn/no-keyword-prefix */
|
||||||
import { type Dispatch, type ReactNode, type SetStateAction } from 'react'
|
import { type Dispatch, type ReactNode, type SetStateAction } from 'react'
|
||||||
import {
|
import {
|
||||||
Button as AriaButton,
|
Button as AriaButton,
|
||||||
@ -20,7 +21,9 @@ function Button(properties: ButtonProperties) {
|
|||||||
{...properties}
|
{...properties}
|
||||||
aria-label='Dialog'
|
aria-label='Dialog'
|
||||||
className={cn(
|
className={cn(
|
||||||
'outline-none',
|
'w-fit text-sm rounded-lg px-4 py-2',
|
||||||
|
'bg-main-700 dark:bg-main-800 text-white',
|
||||||
|
properties.isDisabled && 'opacity-50 cursor-not-allowed',
|
||||||
properties.className
|
properties.className
|
||||||
)}
|
)}
|
||||||
// If control is passed, set the state value
|
// If control is passed, set the state value
|
||||||
@ -42,11 +45,12 @@ function Action(properties: ActionProperties) {
|
|||||||
type={properties.variant === 'confirm' ? 'submit' : 'button'}
|
type={properties.variant === 'confirm' ? 'submit' : 'button'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-4 py-2 rounded-lg',
|
'px-4 py-2 rounded-lg',
|
||||||
|
properties.isDisabled && 'opacity-50 cursor-not-allowed',
|
||||||
properties.variant === 'cancel'
|
properties.variant === 'cancel'
|
||||||
? 'text-ui-700 dark:text-ui-300'
|
? 'text-ui-700 dark:text-ui-300'
|
||||||
: 'text-ui-950 dark:text-ui-50',
|
: 'text-ui-300 dark:text-ui-300',
|
||||||
properties.variant === 'confirm'
|
properties.variant === 'confirm'
|
||||||
? 'bg-blue-500 hover:bg-blue-600 pressed:bg-blue-700 text-white'
|
? 'bg-main-700 dark:bg-main-700 pressed:bg-main-800 dark:pressed:bg-main-800'
|
||||||
: 'bg-ui-200 dark:bg-ui-800 pressed:bg-ui-300 dark:pressed:bg-ui-700',
|
: 'bg-ui-200 dark:bg-ui-800 pressed:bg-ui-300 dark:pressed:bg-ui-700',
|
||||||
properties.className
|
properties.className
|
||||||
)}
|
)}
|
||||||
@ -82,9 +86,10 @@ function Text(properties: React.HTMLProps<HTMLParagraphElement>) {
|
|||||||
type PanelProperties = {
|
type PanelProperties = {
|
||||||
readonly children: (close: () => void) => ReactNode;
|
readonly children: (close: () => void) => ReactNode;
|
||||||
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>];
|
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||||
|
readonly className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Panel({ children, control }: PanelProperties) {
|
function Panel({ children, control, className }: PanelProperties) {
|
||||||
return (
|
return (
|
||||||
<ModalOverlay
|
<ModalOverlay
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
@ -92,8 +97,9 @@ function Panel({ children, control }: PanelProperties) {
|
|||||||
'fixed inset-0 h-screen w-screen z-50 bg-black/30',
|
'fixed inset-0 h-screen w-screen z-50 bg-black/30',
|
||||||
'flex items-center justify-center dark:bg-black/70',
|
'flex items-center justify-center dark:bg-black/70',
|
||||||
'entering:animate-in exiting:animate-out',
|
'entering:animate-in exiting:animate-out',
|
||||||
'entering:fade-in entering:duration-300 entering:ease-out',
|
'entering:fade-in entering:duration-200 entering:ease-out',
|
||||||
'exiting:fade-out exiting:duration-200 exiting:ease-in'
|
'exiting:fade-out exiting:duration-100 exiting:ease-in',
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
isOpen={control ? control[0] : undefined}
|
isOpen={control ? control[0] : undefined}
|
||||||
onOpenChange={control ? control[1] : undefined}
|
onOpenChange={control ? control[1] : undefined}
|
||||||
@ -104,8 +110,8 @@ function Panel({ children, control }: PanelProperties) {
|
|||||||
'bg-ui-50 dark:bg-ui-900 shadow-lg',
|
'bg-ui-50 dark:bg-ui-900 shadow-lg',
|
||||||
'entering:animate-in exiting:animate-out',
|
'entering:animate-in exiting:animate-out',
|
||||||
'dark:border dark:border-ui-700',
|
'dark:border dark:border-ui-700',
|
||||||
'entering:zoom-in-95 entering:ease-out entering:duration-300',
|
'entering:zoom-in-95 entering:ease-out entering:duration-200',
|
||||||
'exiting:zoom-out-95 exiting:ease-in exiting:duration-200'
|
'exiting:zoom-out-95 exiting:ease-in exiting:duration-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AriaDialog role='alertdialog' className='outline-none relative'>
|
<AriaDialog role='alertdialog' className='outline-none relative'>
|
||||||
|
|||||||
@ -1,66 +1,52 @@
|
|||||||
import { Transition } from '@headlessui/react'
|
|
||||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||||
import { isRouteErrorResponse, useRouteError } from '@remix-run/react'
|
import { isRouteErrorResponse, useRouteError } from '@remix-run/react'
|
||||||
import clsx from 'clsx'
|
import { useState } from 'react'
|
||||||
import { Fragment, useEffect, useState } from 'react'
|
|
||||||
|
import { cn } from '~/utils/cn'
|
||||||
|
|
||||||
import Code from './Code'
|
import Code from './Code'
|
||||||
|
import Dialog from './Dialog'
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly type?: 'full' | 'embedded';
|
readonly type?: 'full' | 'embedded';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorPopup({ type = 'full' }: Properties) {
|
export function ErrorPopup({ type = 'full' }: Properties) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
// eslint-disable-next-line react/hook-use-state
|
||||||
|
const open = useState(true)
|
||||||
|
|
||||||
const error = useRouteError()
|
const error = useRouteError()
|
||||||
const routing = isRouteErrorResponse(error)
|
const routing = isRouteErrorResponse(error)
|
||||||
const message = (error instanceof Error ? error.message : 'An unexpected error occurred')
|
const message = (error instanceof Error ? error.message : 'An unexpected error occurred')
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
// Debounce the error modal so it doesn't show up for a split second
|
|
||||||
// when the user navigates to a new page.
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsOpen(true)
|
|
||||||
}, 150)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition as={Fragment} show={isOpen}>
|
<Dialog>
|
||||||
<Transition.Child
|
<Dialog.Panel
|
||||||
as={Fragment}
|
className={cn(
|
||||||
enter='ease-out duration-150'
|
type === 'embedded' ? 'pointer-events-none bg-transparent dark:bg-transparent' : '',
|
||||||
enterFrom='opacity-0 scale-95'
|
|
||||||
enterTo='opacity-100 scale-100'
|
|
||||||
>
|
|
||||||
|
|
||||||
<div className={clsx(
|
|
||||||
'flex items-center justify-center overflow-clip',
|
|
||||||
type === 'full' ? 'min-h-screen' : 'mt-24'
|
|
||||||
)}
|
)}
|
||||||
>
|
control={open}
|
||||||
<div className={clsx(
|
>
|
||||||
'flex flex-col items-center justify-center space-y-2 w-full sm:w-1/2 xl:w-1/3',
|
{() => (
|
||||||
'bg-white dark:bg-zinc-800 rounded-lg py-8 px-4 md:px-16',
|
<>
|
||||||
'border border-gray-200 dark:border-zinc-700 text-center'
|
<div className='flex items-center justify-between'>
|
||||||
)}
|
<Dialog.Title className='text-3xl mb-0'>
|
||||||
>
|
{routing ? error.status : 'Error'}
|
||||||
<ExclamationTriangleIcon className='w-12 h-12 text-red-500'/>
|
</Dialog.Title>
|
||||||
<h1 className='text-2xl font-semibold text-gray-800 dark:text-gray-100'>
|
<ExclamationTriangleIcon className='w-12 h-12 text-red-500'/>
|
||||||
{routing ? error.status : 'Error'}
|
</div>
|
||||||
</h1>
|
<Dialog.Text className='mt-4 text-lg'>
|
||||||
{routing ? (
|
{routing ? (
|
||||||
<p className='text-gray-500 dark:text-gray-400'>
|
error.statusText
|
||||||
{error.statusText}
|
) : (
|
||||||
</p>
|
<Code>
|
||||||
) : (
|
{message}
|
||||||
<Code className='text-sm'>
|
</Code>
|
||||||
{message}
|
)}
|
||||||
</Code>
|
</Dialog.Text>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Dialog>
|
||||||
</Transition>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type ReactNode } from 'react'
|
import { type Dispatch, type ReactNode, type SetStateAction } from 'react'
|
||||||
import {
|
import {
|
||||||
Button as AriaButton,
|
Button as AriaButton,
|
||||||
Menu as AriaMenu,
|
Menu as AriaMenu,
|
||||||
@ -49,6 +49,31 @@ function Items(properties: Parameters<typeof AriaMenu>[0]) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ButtonProperties = Parameters<typeof AriaButton>[0] & {
|
||||||
|
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemButton(properties: ButtonProperties) {
|
||||||
|
return (
|
||||||
|
<MenuItem className='outline-none'>
|
||||||
|
<AriaButton
|
||||||
|
{...properties}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 w-full outline-none text-left',
|
||||||
|
'hover:bg-ui-200 dark:hover:bg-ui-700',
|
||||||
|
properties.className
|
||||||
|
)}
|
||||||
|
aria-label='Menu Dialog'
|
||||||
|
// If control is passed, set the state value
|
||||||
|
onPress={event => {
|
||||||
|
properties.onPress?.(event)
|
||||||
|
properties.control?.[1](true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Item(properties: Parameters<typeof MenuItem>[0]) {
|
function Item(properties: Parameters<typeof MenuItem>[0]) {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -70,4 +95,4 @@ function Menu({ children }: { readonly children: ReactNode }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Object.assign(Menu, { Button, Item, Items })
|
export default Object.assign(Menu, { Button, Item, ItemButton, Items })
|
||||||
|
|||||||
40
app/components/Switch.tsx
Normal file
40
app/components/Switch.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Switch as AriaSwitch } from 'react-aria-components'
|
||||||
|
|
||||||
|
import { cn } from '~/utils/cn'
|
||||||
|
|
||||||
|
type SwitchProperties = Parameters<typeof AriaSwitch>[0] & {
|
||||||
|
readonly label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Switch(properties: SwitchProperties) {
|
||||||
|
return (
|
||||||
|
<AriaSwitch
|
||||||
|
{...properties}
|
||||||
|
aria-label={properties.label}
|
||||||
|
className='group flex gap-2 items-center'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-[26px] w-[44px] shrink-0 cursor-default',
|
||||||
|
'rounded-full shadow-inner bg-clip-padding',
|
||||||
|
'border border-solid border-white/30 p-[3px]',
|
||||||
|
'box-border transition duration-100 ease-in-out',
|
||||||
|
'outline-none group-focus-visible:ring-2 ring-black',
|
||||||
|
|
||||||
|
'bg-main-700 dark:bg-main-800',
|
||||||
|
'group-pressed:bg-main-800 dark:group-pressed:bg-main-900',
|
||||||
|
'group-selected:bg-main-900 group-selected:group-pressed:bg-main-900',
|
||||||
|
properties.isDisabled && 'opacity-50 cursor-not-allowed',
|
||||||
|
properties.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn(
|
||||||
|
'h-[18px] w-[18px] transform rounded-full',
|
||||||
|
'bg-white shadow transition duration-100',
|
||||||
|
'ease-in-out translate-x-0 group-selected:translate-x-[100%]'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AriaSwitch>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ export default function TextField(properties: TextFieldProperties) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'block px-2.5 py-1.5 w-full rounded-lg my-1',
|
'block px-2.5 py-1.5 w-full rounded-lg my-1',
|
||||||
'border border-ui-200 dark:border-ui-600',
|
'border border-ui-200 dark:border-ui-600',
|
||||||
|
'dark:bg-ui-800 dark:text-ui-300',
|
||||||
properties.className
|
properties.className
|
||||||
)}
|
)}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
|
|||||||
@ -1,47 +1,79 @@
|
|||||||
import { useToaster } from 'react-hot-toast/headless'
|
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { type AriaToastProps, useToast, useToastRegion } from '@react-aria/toast'
|
||||||
|
import { ToastQueue, type ToastState, useToastQueue } from '@react-stately/toast'
|
||||||
|
import { type ReactNode, useRef } from 'react'
|
||||||
|
import { Button } from 'react-aria-components'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { ClientOnly } from 'remix-utils/client-only'
|
||||||
|
|
||||||
export default function Toaster() {
|
import { cn } from '~/utils/cn'
|
||||||
const { toasts, handlers } = useToaster()
|
|
||||||
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
type ToastProperties = AriaToastProps<ReactNode> & {
|
||||||
|
readonly state: ToastState<ReactNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toast({ state, ...properties }: ToastProperties) {
|
||||||
|
const reference = useRef(null)
|
||||||
|
const { toastProps, titleProps, closeButtonProps } = useToast(properties, state, reference)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='fixed bottom-0 right-0 p-4 w-80 h-1/2 overflow-hidden pointer-events-none'
|
{...toastProps}
|
||||||
onMouseEnter={startPause}
|
ref={reference}
|
||||||
onMouseLeave={endPause}
|
className={cn(
|
||||||
|
'bg-main-700 dark:bg-main-800 rounded-lg',
|
||||||
|
'text-main-100 dark:text-main-200 z-50',
|
||||||
|
'border border-main-600 dark:border-main-700',
|
||||||
|
'flex items-center justify-between p-3 pl-4 w-80'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{toasts.slice(0, 6).map(toast => {
|
<div {...titleProps}>{properties.toast.content}</div>
|
||||||
const offset = calculateOffset(toast, {
|
<Button
|
||||||
reverseOrder: false,
|
{...closeButtonProps}
|
||||||
gutter: -8
|
className={cn(
|
||||||
})
|
'outline-none rounded-full p-1',
|
||||||
|
'hover:bg-main-600 dark:hover:bg-main-700'
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
)}
|
||||||
const reference = (element: HTMLDivElement | null) => {
|
>
|
||||||
if (element && typeof toast.height !== 'number') {
|
<XMarkIcon className='w-4 h-4'/>
|
||||||
const { height } = element.getBoundingClientRect()
|
</Button>
|
||||||
updateHeight(toast.id, -height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={toast.id}
|
|
||||||
ref={reference}
|
|
||||||
className='fixed bottom-4 right-4 p-4 bg-gray-800 rounded-lg text-white transition-all duration-300'
|
|
||||||
{...toast.ariaProps}
|
|
||||||
style={{
|
|
||||||
transform: `translateY(${offset}px) translateX(${toast.visible ? 0 : 200}%)`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{typeof toast.message === 'function' ? (
|
|
||||||
toast.message(toast)
|
|
||||||
) : (
|
|
||||||
toast.message
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toasts = new ToastQueue<ReactNode>({
|
||||||
|
maxVisibleToasts: 5
|
||||||
|
})
|
||||||
|
|
||||||
|
export function toast(text: string) {
|
||||||
|
return toasts.add(text, { timeout: 5000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const reference = useRef(null)
|
||||||
|
const state = useToastQueue(toasts)
|
||||||
|
const { regionProps } = useToastRegion({}, state, reference)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientOnly>
|
||||||
|
{() => createPortal(
|
||||||
|
state.visibleToasts.length >= 0 ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed bottom-4 right-4',
|
||||||
|
'flex flex-col gap-4'
|
||||||
|
)}
|
||||||
|
{...regionProps}
|
||||||
|
ref={reference}
|
||||||
|
>
|
||||||
|
{state.visibleToasts.map(toast => (
|
||||||
|
<Toast key={toast.key} toast={toast} state={state}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : undefined,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</ClientOnly>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@remix-run/react'
|
} from '@remix-run/react'
|
||||||
|
|
||||||
import { ErrorPopup } from '~/components/Error'
|
import { ErrorPopup } from '~/components/Error'
|
||||||
import Toaster from '~/components/Toaster'
|
import { Toaster } from '~/components/Toaster'
|
||||||
import stylesheet from '~/tailwind.css?url'
|
import stylesheet from '~/tailwind.css?url'
|
||||||
import { getContext, registerConfigWatcher } from '~/utils/config'
|
import { getContext, registerConfigWatcher } from '~/utils/config'
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import CodeMirror from '@uiw/react-codemirror'
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import CodeMirrorMerge from 'react-codemirror-merge'
|
import CodeMirrorMerge from 'react-codemirror-merge'
|
||||||
import { toast } from 'react-hot-toast/headless'
|
|
||||||
|
|
||||||
import Button from '~/components/Button'
|
import Button from '~/components/Button'
|
||||||
import Spinner from '~/components/Spinner'
|
import Spinner from '~/components/Spinner'
|
||||||
|
import { toast } from '~/components/Toaster'
|
||||||
|
|
||||||
import Fallback from './fallback'
|
import Fallback from './fallback'
|
||||||
|
|
||||||
@ -48,28 +48,24 @@ export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
|
|||||||
<>
|
<>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'border border-gray-200 dark:border-gray-700',
|
'border border-gray-200 dark:border-gray-700',
|
||||||
'rounded-b-lg rounded-tr-lg mb-2 overflow-hidden'
|
'rounded-b-lg rounded-tr-lg mb-2 z-10 overflow-x-hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{loading ? (
|
<div className='overflow-y-scroll h-editor text-sm'>
|
||||||
<Fallback acl={acl} where='client'/>
|
{loading ? (
|
||||||
) : (
|
<Fallback acl={acl} where='client'/>
|
||||||
mode === 'edit' ? (
|
|
||||||
<CodeMirror
|
|
||||||
value={acl}
|
|
||||||
className='h-editor text-sm'
|
|
||||||
theme={light ? githubLight : githubDark}
|
|
||||||
extensions={[aclType]}
|
|
||||||
readOnly={!data.hasAclWrite}
|
|
||||||
onChange={value => {
|
|
||||||
setAcl(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
mode === 'edit' ? (
|
||||||
className='overflow-y-scroll'
|
<CodeMirror
|
||||||
style={{ height: 'calc(100vh - 20rem)' }}
|
value={acl}
|
||||||
>
|
theme={light ? githubLight : githubDark}
|
||||||
|
extensions={[aclType]}
|
||||||
|
readOnly={!data.hasAclWrite}
|
||||||
|
onChange={value => {
|
||||||
|
setAcl(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<CodeMirrorMerge
|
<CodeMirrorMerge
|
||||||
theme={light ? githubLight : githubDark}
|
theme={light ? githubLight : githubDark}
|
||||||
orientation='a-b'
|
orientation='a-b'
|
||||||
@ -85,9 +81,9 @@ export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
|
|||||||
extensions={[aclType]}
|
extensions={[aclType]}
|
||||||
/>
|
/>
|
||||||
</CodeMirrorMerge>
|
</CodeMirrorMerge>
|
||||||
</div>
|
)
|
||||||
)
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { Tab } from '@headlessui/react'
|
|
||||||
import { BeakerIcon, CubeTransparentIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/outline'
|
import { BeakerIcon, CubeTransparentIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/outline'
|
||||||
import { type ActionFunctionArgs, json } from '@remix-run/node'
|
import { type ActionFunctionArgs, json } from '@remix-run/node'
|
||||||
import { useLoaderData } from '@remix-run/react'
|
import { useLoaderData } from '@remix-run/react'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Fragment } from 'react/jsx-runtime'
|
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'
|
||||||
import { ClientOnly } from 'remix-utils/client-only'
|
import { ClientOnly } from 'remix-utils/client-only'
|
||||||
|
|
||||||
import Notice from '~/components/Notice'
|
import Notice from '~/components/Notice'
|
||||||
|
import { cn } from '~/utils/cn'
|
||||||
import { getAcl, getContext, patchAcl } from '~/utils/config'
|
import { getAcl, getContext, patchAcl } from '~/utils/config'
|
||||||
import { sighupHeadscale } from '~/utils/docker'
|
import { sighupHeadscale } from '~/utils/docker'
|
||||||
import { getSession } from '~/utils/sessions'
|
import { getSession } from '~/utils/sessions'
|
||||||
@ -87,100 +86,80 @@ export default function Page() {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Tab.Group>
|
<Tabs>
|
||||||
<Tab.List className={clsx(
|
<TabList className={cn(
|
||||||
'flex border-t border-gray-200 dark:border-gray-700',
|
'flex border-t border-gray-200 dark:border-gray-700',
|
||||||
'w-fit rounded-t-lg overflow-hidden',
|
'w-fit rounded-t-lg overflow-hidden',
|
||||||
'text-gray-400 dark:text-gray-500'
|
'text-gray-400 dark:text-gray-500'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tab as={Fragment}>
|
<Tab
|
||||||
{({ selected }) => (
|
id='edit'
|
||||||
<button
|
className={({ isSelected }) => cn(
|
||||||
type='button'
|
'px-4 py-2 rounded-tl-lg',
|
||||||
className={clsx(
|
'focus:outline-none flex items-center gap-2',
|
||||||
'px-4 py-2 rounded-tl-lg',
|
'border-x border-gray-200 dark:border-gray-700',
|
||||||
'focus:outline-none flex items-center gap-2',
|
isSelected ? 'text-gray-900 dark:text-gray-100' : ''
|
||||||
'border-l border-gray-200 dark:border-gray-700',
|
|
||||||
selected ? 'text-gray-900 dark:text-gray-100' : ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<PencilSquareIcon className='w-5 h-5'/>
|
|
||||||
<p>
|
|
||||||
Edit file
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<PencilSquareIcon className='w-5 h-5'/>
|
||||||
|
<p>Edit file</p>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab as={Fragment}>
|
<Tab
|
||||||
{({ selected }) => (
|
id='diff'
|
||||||
<button
|
className={({ isSelected }) => cn(
|
||||||
type='button'
|
'px-4 py-2',
|
||||||
className={clsx(
|
'focus:outline-none flex items-center gap-2',
|
||||||
'px-4 py-2',
|
'border-x border-gray-200 dark:border-gray-700',
|
||||||
'focus:outline-none flex items-center gap-2',
|
isSelected ? 'text-gray-900 dark:text-gray-100' : ''
|
||||||
'border-x border-gray-200 dark:border-gray-700',
|
|
||||||
selected ? 'text-gray-900 dark:text-gray-100' : ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<EyeIcon className='w-5 h-5'/>
|
|
||||||
<p>
|
|
||||||
Preview changes
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<EyeIcon className='w-5 h-5'/>
|
||||||
|
<p>Preview changes</p>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab as={Fragment}>
|
<Tab
|
||||||
{({ selected }) => (
|
id='preview'
|
||||||
<button
|
className={({ isSelected }) => cn(
|
||||||
type='button'
|
'px-4 py-2 rounded-tr-lg',
|
||||||
className={clsx(
|
'focus:outline-none flex items-center gap-2',
|
||||||
'px-4 py-2 rounded-tr-lg',
|
'border-x border-gray-200 dark:border-gray-700',
|
||||||
'focus:outline-none flex items-center gap-2',
|
isSelected ? 'text-gray-900 dark:text-gray-100' : ''
|
||||||
'border-r border-gray-200 dark:border-gray-700',
|
|
||||||
selected ? 'text-gray-900 dark:text-gray-100' : ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<BeakerIcon className='w-5 h-5'/>
|
|
||||||
<p>
|
|
||||||
Preview rules
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<BeakerIcon className='w-5 h-5'/>
|
||||||
|
<p>Preview rules</p>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</TabList>
|
||||||
<Tab.Panels>
|
<TabPanel id='edit'>
|
||||||
<Tab.Panel>
|
<ClientOnly fallback={<Fallback acl={acl} where='server'/>}>
|
||||||
<ClientOnly fallback={<Fallback acl={acl} where='server'/>}>
|
{() => (
|
||||||
{() => (
|
<Editor data={data} acl={acl} setAcl={setAcl} mode='edit'/>
|
||||||
<Editor data={data} acl={acl} setAcl={setAcl} mode='edit'/>
|
)}
|
||||||
)}
|
</ClientOnly>
|
||||||
</ClientOnly>
|
</TabPanel>
|
||||||
</Tab.Panel>
|
<TabPanel id='diff'>
|
||||||
<Tab.Panel>
|
<ClientOnly fallback={<Fallback acl={acl} where='server'/>}>
|
||||||
<ClientOnly fallback={<Fallback acl={acl} where='server'/>}>
|
{() => (
|
||||||
{() => (
|
<Editor data={data} acl={acl} setAcl={setAcl} mode='diff'/>
|
||||||
<Editor data={data} acl={acl} setAcl={setAcl} mode='diff'/>
|
)}
|
||||||
)}
|
</ClientOnly>
|
||||||
</ClientOnly>
|
</TabPanel>
|
||||||
</Tab.Panel>
|
<TabPanel id='preview'>
|
||||||
<Tab.Panel>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={clsx(
|
'border border-gray-200 dark:border-gray-700',
|
||||||
'border border-gray-200 dark:border-gray-700',
|
'rounded-b-lg rounded-tr-lg mb-4 overflow-hidden',
|
||||||
'rounded-b-lg rounded-tr-lg mb-4 overflow-hidden',
|
'p-16 flex flex-col items-center justify-center'
|
||||||
'p-16 flex flex-col items-center justify-center'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<CubeTransparentIcon className='w-24 h-24 text-gray-300 dark:text-gray-500'/>
|
||||||
<CubeTransparentIcon className='w-24 h-24 text-gray-300 dark:text-gray-500'/>
|
<p className='w-1/2 text-center mt-4'>
|
||||||
<p className='w-1/2 text-center mt-4'>
|
The Preview rules is very much still a work in progress.
|
||||||
The Preview rules is very much still a work in progress.
|
It is a bit complicated to implement right now but hopefully it will be available soon.
|
||||||
It is a bit complicated to implement right now but hopefully it will be available soon.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</TabPanel>
|
||||||
</Tab.Panel>
|
</Tabs>
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export default function Modal({ isEnabled, disabled }: Properties) {
|
|||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-fit text-sm rounded-lg px-4 py-2',
|
'w-fit text-sm rounded-lg px-4 py-2',
|
||||||
'bg-gray-700 dark:bg-gray-800 text-white',
|
'bg-main-700 dark:bg-main-800 text-white',
|
||||||
disabled && 'opacity-50 cursor-not-allowed'
|
disabled && 'opacity-50 cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export default function Modal({ name, disabled }: Properties) {
|
|||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-fit text-sm rounded-lg px-4 py-2',
|
'w-fit text-sm rounded-lg px-4 py-2',
|
||||||
'bg-gray-700 dark:bg-gray-800 text-white',
|
'bg-main-700 dark:bg-main-800 text-white',
|
||||||
disabled && 'opacity-50 cursor-not-allowed'
|
disabled && 'opacity-50 cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { Switch } from '@headlessui/react'
|
|
||||||
import { type ActionFunctionArgs } from '@remix-run/node'
|
import { type ActionFunctionArgs } from '@remix-run/node'
|
||||||
import { json, useFetcher, useLoaderData } from '@remix-run/react'
|
import { json, useFetcher, useLoaderData } from '@remix-run/react'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import Button from '~/components/Button'
|
import Button from '~/components/Button'
|
||||||
@ -9,6 +7,7 @@ import Code from '~/components/Code'
|
|||||||
import Input from '~/components/Input'
|
import Input from '~/components/Input'
|
||||||
import Notice from '~/components/Notice'
|
import Notice from '~/components/Notice'
|
||||||
import Spinner from '~/components/Spinner'
|
import Spinner from '~/components/Spinner'
|
||||||
|
import Switch from '~/components/Switch'
|
||||||
import TableList from '~/components/TableList'
|
import TableList from '~/components/TableList'
|
||||||
import { getConfig, getContext, patchConfig } from '~/utils/config'
|
import { getConfig, getContext, patchConfig } from '~/utils/config'
|
||||||
import { restartHeadscale } from '~/utils/docker'
|
import { restartHeadscale } from '~/utils/docker'
|
||||||
@ -97,12 +96,9 @@ export default function Page() {
|
|||||||
Override local DNS
|
Override local DNS
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={localOverride}
|
label='Override local DNS'
|
||||||
disabled={!data.hasConfigWrite}
|
defaultSelected={localOverride}
|
||||||
className={clsx(
|
isDisabled={!data.hasConfigWrite}
|
||||||
localOverride ? 'bg-gray-800 dark:bg-gray-600' : 'bg-gray-200 dark:bg-gray-400',
|
|
||||||
'relative inline-flex h-4 w-9 items-center rounded-full'
|
|
||||||
)}
|
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
fetcher.submit({
|
fetcher.submit({
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@ -114,15 +110,7 @@ export default function Page() {
|
|||||||
|
|
||||||
setLocalOverride(!localOverride)
|
setLocalOverride(!localOverride)
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<span className='sr-only'>Override local DNS</span>
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
localOverride ? 'translate-x-6' : 'translate-x-1',
|
|
||||||
'inline-block h-2 w-2 transform rounded-full bg-white transition'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TableList>
|
<TableList>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UserCircleIcon, UsersIcon } from '@heroicons/react/24/outline'
|
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UserCircleIcon, UsersIcon } from '@heroicons/react/24/outline'
|
||||||
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
||||||
import { Form, Outlet, useLoaderData, useRouteError } from '@remix-run/react'
|
import { Form, Outlet, useLoaderData } from '@remix-run/react'
|
||||||
|
|
||||||
import { ErrorPopup } from '~/components/Error'
|
import { ErrorPopup } from '~/components/Error'
|
||||||
import Menu from '~/components/Menu'
|
import Menu from '~/components/Menu'
|
||||||
@ -43,9 +43,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const data = useLoaderData<typeof loader>()
|
const data = useLoaderData<typeof loader>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className='mb-6 bg-gray-800 text-white dark:bg-gray-700'>
|
<header className='mb-6 bg-main-700 dark:bg-main-800 text-white'>
|
||||||
<nav className='container mx-auto'>
|
<nav className='container mx-auto'>
|
||||||
<div className='flex items-center justify-between mb-8 pt-4'>
|
<div className='flex items-center justify-between mb-8 pt-4'>
|
||||||
<div className='flex items-center gap-x-2'>
|
<div className='flex items-center gap-x-2'>
|
||||||
@ -71,7 +72,7 @@ export default function Layout() {
|
|||||||
<p className='font-bold'>{data.user?.name}</p>
|
<p className='font-bold'>{data.user?.name}</p>
|
||||||
<p>{data.user?.email}</p>
|
<p>{data.user?.email}</p>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item className='text-red-700 cursor-pointer'>
|
<Menu.Item className='text-red-500 dark:text-red-400'>
|
||||||
<Form method='POST' action='/logout'>
|
<Form method='POST' action='/logout'>
|
||||||
<button type='submit' className='w-full text-right'>
|
<button type='submit' className='w-full text-right'>
|
||||||
Logout
|
Logout
|
||||||
@ -104,15 +105,9 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary() {
|
export function ErrorBoundary() {
|
||||||
const data = useLoaderData<typeof loader>()
|
|
||||||
const error = useRouteError()
|
|
||||||
if (!data) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
<header className='mb-16 bg-main-700 dark:bg-main-800 text-white'>
|
||||||
<nav className='container mx-auto'>
|
<nav className='container mx-auto'>
|
||||||
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
||||||
<CpuChipIcon className='w-8 h-8'/>
|
<CpuChipIcon className='w-8 h-8'/>
|
||||||
@ -121,13 +116,9 @@ export function ErrorBoundary() {
|
|||||||
<div className='flex items-center gap-x-4'>
|
<div className='flex items-center gap-x-4'>
|
||||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
||||||
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
||||||
{data.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/> : undefined}
|
<TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/>
|
||||||
{data.hasConfig ? (
|
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
||||||
<>
|
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
||||||
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
|
||||||
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -17,8 +17,9 @@
|
|||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@headlessui/react": "^1.7.18",
|
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.1.3",
|
||||||
|
"@react-aria/toast": "3.0.0-beta.10",
|
||||||
|
"@react-stately/toast": "3.0.0-beta.2",
|
||||||
"@remix-run/node": "^2.8.1",
|
"@remix-run/node": "^2.8.1",
|
||||||
"@remix-run/react": "^2.8.1",
|
"@remix-run/react": "^2.8.1",
|
||||||
"@remix-run/serve": "^2.8.1",
|
"@remix-run/serve": "^2.8.1",
|
||||||
@ -31,7 +32,6 @@
|
|||||||
"react-aria-components": "^1.1.1",
|
"react-aria-components": "^1.1.1",
|
||||||
"react-codemirror-merge": "^4.21.25",
|
"react-codemirror-merge": "^4.21.25",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
|
||||||
"remix-utils": "^7.6.0",
|
"remix-utils": "^7.6.0",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-react-aria-components": "^1.1.1",
|
"tailwindcss-react-aria-components": "^1.1.1",
|
||||||
|
|||||||
101
pnpm-lock.yaml
101
pnpm-lock.yaml
@ -23,12 +23,15 @@ dependencies:
|
|||||||
'@dnd-kit/utilities':
|
'@dnd-kit/utilities':
|
||||||
specifier: ^3.2.2
|
specifier: ^3.2.2
|
||||||
version: 3.2.2(react@18.2.0)
|
version: 3.2.2(react@18.2.0)
|
||||||
'@headlessui/react':
|
|
||||||
specifier: ^1.7.18
|
|
||||||
version: 1.7.18(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@heroicons/react':
|
'@heroicons/react':
|
||||||
specifier: ^2.1.3
|
specifier: ^2.1.3
|
||||||
version: 2.1.3(react@18.2.0)
|
version: 2.1.3(react@18.2.0)
|
||||||
|
'@react-aria/toast':
|
||||||
|
specifier: 3.0.0-beta.10
|
||||||
|
version: 3.0.0-beta.10(react@18.2.0)
|
||||||
|
'@react-stately/toast':
|
||||||
|
specifier: 3.0.0-beta.2
|
||||||
|
version: 3.0.0-beta.2(react@18.2.0)
|
||||||
'@remix-run/node':
|
'@remix-run/node':
|
||||||
specifier: ^2.8.1
|
specifier: ^2.8.1
|
||||||
version: 2.8.1(typescript@5.4.3)
|
version: 2.8.1(typescript@5.4.3)
|
||||||
@ -65,9 +68,6 @@ dependencies:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
react-hot-toast:
|
|
||||||
specifier: ^2.4.1
|
|
||||||
version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
remix-utils:
|
remix-utils:
|
||||||
specifier: ^7.6.0
|
specifier: ^7.6.0
|
||||||
version: 7.6.0(@remix-run/node@2.8.1)(@remix-run/react@2.8.1)(react@18.2.0)
|
version: 7.6.0(@remix-run/node@2.8.1)(@remix-run/react@2.8.1)(react@18.2.0)
|
||||||
@ -1110,19 +1110,6 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16 || ^17 || ^18
|
|
||||||
react-dom: ^16 || ^17 || ^18
|
|
||||||
dependencies:
|
|
||||||
'@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
client-only: 0.0.1
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@heroicons/react@2.1.3(react@18.2.0):
|
/@heroicons/react@2.1.3(react@18.2.0):
|
||||||
resolution: {integrity: sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==}
|
resolution: {integrity: sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1606,6 +1593,18 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@react-aria/landmark@3.0.0-beta.10(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-qFfAVgCUP/d+sFXCYVJHOMA8fD9VGBWcbJIbfz14X0sdyJ1gj5SL/m4o7cX3OsD9tgPz2A33c1FfmO2gN/qOVg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@react-aria/utils': 3.23.2(react@18.2.0)
|
||||||
|
'@react-types/shared': 3.22.1(react@18.2.0)
|
||||||
|
'@swc/helpers': 0.5.10
|
||||||
|
react: 18.2.0
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@react-aria/link@3.6.5(react@18.2.0):
|
/@react-aria/link@3.6.5(react@18.2.0):
|
||||||
resolution: {integrity: sha512-kg8CxKqkciQFzODvLAfxEs8gbqNXFZCW/ISOE2LHYKbh9pA144LVo71qO3SPeYVVzIjmZeW4vEMdZwqkNozecw==}
|
resolution: {integrity: sha512-kg8CxKqkciQFzODvLAfxEs8gbqNXFZCW/ISOE2LHYKbh9pA144LVo71qO3SPeYVVzIjmZeW4vEMdZwqkNozecw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1958,6 +1957,22 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@react-aria/toast@3.0.0-beta.10(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-b4PZaCZBc8oiiZ4xxWA0pusPj0VBiNbGJsmyQSe9L0OS7/wL+Z+70xpUMzL+RzhtRuP/lNx7G9a92HwcxFfrog==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@react-aria/i18n': 3.10.2(react@18.2.0)
|
||||||
|
'@react-aria/interactions': 3.21.1(react@18.2.0)
|
||||||
|
'@react-aria/landmark': 3.0.0-beta.10(react@18.2.0)
|
||||||
|
'@react-aria/utils': 3.23.2(react@18.2.0)
|
||||||
|
'@react-stately/toast': 3.0.0-beta.2(react@18.2.0)
|
||||||
|
'@react-types/button': 3.9.2(react@18.2.0)
|
||||||
|
'@react-types/shared': 3.22.1(react@18.2.0)
|
||||||
|
'@swc/helpers': 0.5.10
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@react-aria/toggle@3.10.2(react@18.2.0):
|
/@react-aria/toggle@3.10.2(react@18.2.0):
|
||||||
resolution: {integrity: sha512-DgitscHWgI6IFgnvp2HcMpLGX/cAn+XX9kF5RJQbRQ9NqUgruU5cEEGSOLMrEJ6zXDa2xmOiQ+kINcyNhA+JLg==}
|
resolution: {integrity: sha512-DgitscHWgI6IFgnvp2HcMpLGX/cAn+XX9kF5RJQbRQ9NqUgruU5cEEGSOLMrEJ6zXDa2xmOiQ+kINcyNhA+JLg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2284,6 +2299,16 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@react-stately/toast@3.0.0-beta.2(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-LJT3VJ01VeQHTDt+6NwmRn4ma0plZWcWByAeJ24dLYJ0gUpHhHyCCIBB/C1BwZ3/eaNKn7/Crp5FONA6bBzDaA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.10
|
||||||
|
react: 18.2.0
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@react-stately/toggle@3.7.2(react@18.2.0):
|
/@react-stately/toggle@3.7.2(react@18.2.0):
|
||||||
resolution: {integrity: sha512-SHCF2btcoK57c4lyhucRbyPBAFpp0Pdp0vcPdn3hUgqbu6e5gE0CwG/mgFmZRAQoc7PRc7XifL0uNw8diJJI0Q==}
|
resolution: {integrity: sha512-SHCF2btcoK57c4lyhucRbyPBAFpp0Pdp0vcPdn3hUgqbu6e5gE0CwG/mgFmZRAQoc7PRc7XifL0uNw8diJJI0Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2925,21 +2950,6 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
dependencies:
|
|
||||||
'@tanstack/virtual-core': 3.2.0
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@tanstack/virtual-core@3.2.0:
|
|
||||||
resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/acorn@4.0.6:
|
/@types/acorn@4.0.6:
|
||||||
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
|
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3884,6 +3894,7 @@ packages:
|
|||||||
|
|
||||||
/csstype@3.1.3:
|
/csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/data-uri-to-buffer@3.0.1:
|
/data-uri-to-buffer@3.0.1:
|
||||||
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
||||||
@ -4926,14 +4937,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/goober@2.1.14(csstype@3.1.3):
|
|
||||||
resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==}
|
|
||||||
peerDependencies:
|
|
||||||
csstype: ^3.0.10
|
|
||||||
dependencies:
|
|
||||||
csstype: 3.1.3
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/gopd@1.0.1:
|
/gopd@1.0.1:
|
||||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6935,20 +6938,6 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16'
|
|
||||||
react-dom: '>=16'
|
|
||||||
dependencies:
|
|
||||||
goober: 2.1.14(csstype@3.1.3)
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- csstype
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user