refactor: replace dialog components with FormDrawer for improved UI consistency
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 50s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 3m58s
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 50s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 3m58s
Updated multiple components to utilize the new FormDrawer for modal dialogs, enhancing the user interface and streamlining the layout. This change includes the ConfirmDialog, ApiKeyCreateDialog, FirewallRuleCreateDialog, and others, ensuring a more cohesive and modern design across the application. Additionally, refactored the ConfirmDialog to improve confirmation handling and user feedback during actions.
This commit is contained in:
@@ -2,16 +2,10 @@ import { useEffect, useState } from 'react'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { SelectField } from '@/components/select-field'
|
import { SelectField } from '@/components/select-field'
|
||||||
import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels'
|
import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels'
|
||||||
@@ -68,12 +62,22 @@ export function ApiKeyCreateDialog({ open, onOpenChange, onCreated }: ApiKeyCrea
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-sm">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={handleOpenChange}
|
||||||
<DialogTitle>Новый API-ключ</DialogTitle>
|
title="Новый API-ключ"
|
||||||
</DialogHeader>
|
className="sm:max-w-sm"
|
||||||
<div className="flex flex-col gap-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<LoadingButton onClick={save} loading={createMutation.isPending}>
|
||||||
|
Создать
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="key-name">Имя</Label>
|
<Label htmlFor="key-name">Имя</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -100,16 +104,6 @@ export function ApiKeyCreateDialog({ open, onOpenChange, onCreated }: ApiKeyCrea
|
|||||||
onChange={(e) => setExpiresLocal(e.target.value)}
|
onChange={(e) => setExpiresLocal(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</Button>
|
|
||||||
<LoadingButton onClick={save} loading={createMutation.isPending}>
|
|
||||||
Создать
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,126 @@
|
|||||||
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
Drawer,
|
||||||
AlertDialogAction,
|
DrawerClose,
|
||||||
AlertDialogCancel,
|
DrawerContent,
|
||||||
AlertDialogContent,
|
DrawerDescription,
|
||||||
AlertDialogDescription,
|
DrawerFooter,
|
||||||
AlertDialogFooter,
|
DrawerHeader,
|
||||||
AlertDialogHeader,
|
DrawerTitle,
|
||||||
AlertDialogTitle,
|
DrawerTrigger,
|
||||||
AlertDialogTrigger,
|
} from '@evobgp/ui/components/drawer'
|
||||||
} from '@evobgp/ui/components/alert-dialog'
|
|
||||||
import type { ReactElement, ReactNode } from 'react'
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
type ConfirmDialogBaseProps = {
|
||||||
trigger: ReactElement
|
|
||||||
title: string
|
title: string
|
||||||
description?: ReactNode
|
description?: ReactNode
|
||||||
confirmLabel?: string
|
confirmLabel?: string
|
||||||
cancelLabel?: string
|
cancelLabel?: string
|
||||||
destructive?: boolean
|
destructive?: boolean
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
|
confirmDisabled?: boolean
|
||||||
|
confirmLoading?: boolean
|
||||||
|
confirmLoadingLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfirmDialog({
|
type ConfirmDialogWithTrigger = ConfirmDialogBaseProps & {
|
||||||
trigger,
|
trigger: ReactElement
|
||||||
|
open?: never
|
||||||
|
onOpenChange?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmDialogControlled = ConfirmDialogBaseProps & {
|
||||||
|
trigger?: never
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmDialogProps = ConfirmDialogWithTrigger | ConfirmDialogControlled
|
||||||
|
|
||||||
|
function ConfirmDrawerBody({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
confirmLabel = 'Подтвердить',
|
confirmLabel = 'Подтвердить',
|
||||||
cancelLabel = 'Отмена',
|
cancelLabel = 'Отмена',
|
||||||
destructive,
|
destructive,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: ConfirmDialogProps) {
|
confirmDisabled,
|
||||||
|
confirmLoading,
|
||||||
|
confirmLoadingLabel,
|
||||||
|
controlled,
|
||||||
|
}: ConfirmDialogBaseProps & { controlled?: boolean }) {
|
||||||
|
const confirmText =
|
||||||
|
confirmLoading && confirmLoadingLabel
|
||||||
|
? confirmLoadingLabel
|
||||||
|
: confirmLoading
|
||||||
|
? `${confirmLabel}…`
|
||||||
|
: confirmLabel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<>
|
||||||
<AlertDialogTrigger render={trigger} />
|
<DrawerHeader>
|
||||||
<AlertDialogContent>
|
<DrawerTitle>{title}</DrawerTitle>
|
||||||
<AlertDialogHeader>
|
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
</DrawerHeader>
|
||||||
{description ? <AlertDialogDescription>{description}</AlertDialogDescription> : null}
|
<DrawerFooter className="border-t bg-muted/50 sm:flex-row sm:justify-end">
|
||||||
</AlertDialogHeader>
|
<DrawerClose render={<Button variant="outline" disabled={confirmLoading} />}>
|
||||||
<AlertDialogFooter>
|
{cancelLabel}
|
||||||
<AlertDialogCancel>{cancelLabel}</AlertDialogCancel>
|
</DrawerClose>
|
||||||
<AlertDialogAction
|
{controlled ? (
|
||||||
|
<Button
|
||||||
variant={destructive ? 'destructive' : 'default'}
|
variant={destructive ? 'destructive' : 'default'}
|
||||||
|
disabled={confirmDisabled || confirmLoading}
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
>
|
>
|
||||||
{confirmLabel}
|
{confirmText}
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
) : (
|
||||||
</AlertDialogContent>
|
<DrawerClose
|
||||||
</AlertDialog>
|
render={
|
||||||
|
<Button
|
||||||
|
variant={destructive ? 'destructive' : 'default'}
|
||||||
|
disabled={confirmDisabled || confirmLoading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</DrawerClose>
|
||||||
|
)}
|
||||||
|
</DrawerFooter>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||||
|
const bodyProps: ConfirmDialogBaseProps = {
|
||||||
|
title: props.title,
|
||||||
|
description: props.description,
|
||||||
|
confirmLabel: props.confirmLabel,
|
||||||
|
cancelLabel: props.cancelLabel,
|
||||||
|
destructive: props.destructive,
|
||||||
|
onConfirm: props.onConfirm,
|
||||||
|
confirmDisabled: props.confirmDisabled,
|
||||||
|
confirmLoading: props.confirmLoading,
|
||||||
|
confirmLoadingLabel: props.confirmLoadingLabel,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.trigger) {
|
||||||
|
return (
|
||||||
|
<Drawer swipeDirection="right">
|
||||||
|
<DrawerTrigger render={props.trigger} />
|
||||||
|
<DrawerContent className="h-full max-h-none sm:max-w-sm">
|
||||||
|
<ConfirmDrawerBody {...bodyProps} />
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={props.open} onOpenChange={props.onOpenChange} swipeDirection="right">
|
||||||
|
<DrawerContent className="h-full max-h-none sm:max-w-sm">
|
||||||
|
<ConfirmDrawerBody {...bodyProps} controlled />
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
import { SelectField } from '@/components/select-field'
|
import { SelectField } from '@/components/select-field'
|
||||||
@@ -60,12 +54,22 @@ export function FirewallRuleCreateDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-md">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>Новое правило</DialogTitle>
|
title="Новое правило"
|
||||||
</DialogHeader>
|
className="sm:max-w-md"
|
||||||
<div className="flex flex-col gap-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<LoadingButton type="button" onClick={save} loading={createMutation.isPending}>
|
||||||
|
Добавить
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SelectField
|
<SelectField
|
||||||
id="fw-rule-action"
|
id="fw-rule-action"
|
||||||
label="Действие"
|
label="Действие"
|
||||||
@@ -92,16 +96,6 @@ export function FirewallRuleCreateDialog({
|
|||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</Button>
|
|
||||||
<LoadingButton type="button" onClick={save} loading={createMutation.isPending}>
|
|
||||||
Добавить
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@evobgp/ui/lib/utils'
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
} from '@evobgp/ui/components/drawer'
|
||||||
|
import { ScrollArea } from '@evobgp/ui/components/scroll-area'
|
||||||
|
|
||||||
|
interface FormDrawerProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
children: ReactNode
|
||||||
|
footer: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormDrawer({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
className,
|
||||||
|
}: FormDrawerProps) {
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onOpenChange={onOpenChange} swipeDirection="right">
|
||||||
|
<DrawerContent className={cn('h-full max-h-none sm:max-w-lg', className)}>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>{title}</DrawerTitle>
|
||||||
|
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
||||||
|
</DrawerHeader>
|
||||||
|
<ScrollArea className="min-h-0 flex-1 px-4">
|
||||||
|
<div className="space-y-4 pb-4">{children}</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<DrawerFooter className="border-t bg-muted/50 sm:flex-row sm:justify-end">{footer}</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
import { ApiError, apiMutate } from '@/lib/api-client'
|
import { ApiError, apiMutate } from '@/lib/api-client'
|
||||||
@@ -72,15 +65,23 @@ export function ModuleAsEntryDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-sm">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{edit ? 'Редактировать запись' : 'Новая AS-запись'}</DialogTitle>
|
title={edit ? 'Редактировать запись' : 'Новая AS-запись'}
|
||||||
<DialogDescription>
|
description="Номер автономной системы и community для политики анонса."
|
||||||
Номер автономной системы и community для политики анонса.
|
className="sm:max-w-sm"
|
||||||
</DialogDescription>
|
footer={
|
||||||
</DialogHeader>
|
<>
|
||||||
<div className="space-y-4 py-2">
|
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</LoadingButton>
|
||||||
|
<LoadingButton loading={saving} onClick={() => void save()}>
|
||||||
|
{edit ? 'Сохранить' : 'Добавить'}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor="as-asn">ASN</Label>
|
<Label htmlFor="as-asn">ASN</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -101,16 +102,6 @@ export function ModuleAsEntryDialog({
|
|||||||
communities={communities}
|
communities={communities}
|
||||||
nullable
|
nullable
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</LoadingButton>
|
|
||||||
<LoadingButton loading={saving} onClick={() => void save()}>
|
|
||||||
{edit ? 'Сохранить' : 'Добавить'}
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import {
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
import { SelectField } from '@/components/select-field'
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
|
||||||
|
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
|
import { SelectField } from '@/components/select-field'
|
||||||
import { ApiError, apiMutate } from '@/lib/api-client'
|
import { ApiError, apiMutate } from '@/lib/api-client'
|
||||||
import { normalizeCdnSourceKind } from '@/lib/modules/helpers'
|
import { normalizeCdnSourceKind } from '@/lib/modules/helpers'
|
||||||
import type { BgpCommunity, CdnPreviewResponse, CdnSource, CdnSourceCreate } from '@/types/api'
|
import type { BgpCommunity, CdnPreviewResponse, CdnSource, CdnSourceCreate } from '@/types/api'
|
||||||
@@ -154,12 +147,22 @@ export function ModuleCdnSourceDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-lg">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{edit ? 'Редактировать источник' : 'Новый CDN-источник'}</DialogTitle>
|
title={edit ? 'Редактировать источник' : 'Новый CDN-источник'}
|
||||||
</DialogHeader>
|
className="sm:max-w-lg"
|
||||||
<div className="space-y-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</LoadingButton>
|
||||||
|
<LoadingButton loading={saving} onClick={() => void save()}>
|
||||||
|
{edit ? 'Сохранить' : 'Добавить'}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor="cdn-url">URL</Label>
|
<Label htmlFor="cdn-url">URL</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -245,16 +248,6 @@ export function ModuleCdnSourceDialog({
|
|||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</LoadingButton>
|
|
||||||
<LoadingButton loading={saving} onClick={() => void save()}>
|
|
||||||
{edit ? 'Сохранить' : 'Добавить'}
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
import { ApiError, apiMutate } from '@/lib/api-client'
|
import { ApiError, apiMutate } from '@/lib/api-client'
|
||||||
@@ -70,12 +64,22 @@ export function ModuleDomainEntryDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-sm">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{edit ? 'Редактировать домен' : 'Новый домен'}</DialogTitle>
|
title={edit ? 'Редактировать домен' : 'Новый домен'}
|
||||||
</DialogHeader>
|
className="sm:max-w-sm"
|
||||||
<div className="space-y-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</LoadingButton>
|
||||||
|
<LoadingButton loading={saving} onClick={() => void save()}>
|
||||||
|
{edit ? 'Сохранить' : 'Добавить'}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor="dom-fqdn">FQDN</Label>
|
<Label htmlFor="dom-fqdn">FQDN</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -93,16 +97,6 @@ export function ModuleDomainEntryDialog({
|
|||||||
communities={communities}
|
communities={communities}
|
||||||
nullable
|
nullable
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</LoadingButton>
|
|
||||||
<LoadingButton loading={saving} onClick={() => void save()}>
|
|
||||||
{edit ? 'Сохранить' : 'Добавить'}
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,9 @@ import { useState } from 'react'
|
|||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '@evobgp/ui/components/alert-dialog'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
|
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { DataGridCard } from '@/components/data-grid-shell'
|
import { DataGridCard } from '@/components/data-grid-shell'
|
||||||
import { QueryState } from '@/components/query-state'
|
import { QueryState } from '@/components/query-state'
|
||||||
import { TableSkeleton } from '@/components/skeletons'
|
import { TableSkeleton } from '@/components/skeletons'
|
||||||
@@ -218,24 +209,17 @@ export function ModuleEntriesSection({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AlertDialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
<ConfirmDialog
|
||||||
<AlertDialogContent>
|
open={deleteTarget !== null}
|
||||||
<AlertDialogHeader>
|
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||||
<AlertDialogTitle>Удалить запись?</AlertDialogTitle>
|
title="Удалить запись?"
|
||||||
<AlertDialogDescription>{deleteDescription(deleteTarget)}</AlertDialogDescription>
|
description={deleteDescription(deleteTarget)}
|
||||||
</AlertDialogHeader>
|
confirmLabel="Удалить"
|
||||||
<AlertDialogFooter>
|
confirmLoadingLabel="Удаление…"
|
||||||
<AlertDialogCancel disabled={deleting}>Отмена</AlertDialogCancel>
|
destructive
|
||||||
<AlertDialogAction
|
confirmLoading={deleting}
|
||||||
variant="destructive"
|
onConfirm={() => void confirmDelete()}
|
||||||
disabled={deleting}
|
/>
|
||||||
onClick={() => void confirmDelete()}
|
|
||||||
>
|
|
||||||
{deleting ? 'Удаление…' : 'Удалить'}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
import { ApiError, apiMutate } from '@/lib/api-client'
|
import { ApiError, apiMutate } from '@/lib/api-client'
|
||||||
@@ -70,12 +64,22 @@ export function ModuleIpRangeEntryDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-sm">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'}</DialogTitle>
|
title={edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'}
|
||||||
</DialogHeader>
|
className="sm:max-w-sm"
|
||||||
<div className="space-y-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</LoadingButton>
|
||||||
|
<LoadingButton loading={saving} onClick={() => void save()}>
|
||||||
|
{edit ? 'Сохранить' : 'Добавить'}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor="ip-prefix">Префикс (CIDR)</Label>
|
<Label htmlFor="ip-prefix">Префикс (CIDR)</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -93,16 +97,6 @@ export function ModuleIpRangeEntryDialog({
|
|||||||
communities={communities}
|
communities={communities}
|
||||||
placeholder="Выберите community"
|
placeholder="Выберите community"
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</LoadingButton>
|
|
||||||
<LoadingButton loading={saving} onClick={() => void save()}>
|
|
||||||
{edit ? 'Сохранить' : 'Добавить'}
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,11 @@ import { useEffect, useState } from 'react'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import {
|
import { Checkbox } from '@evobgp/ui/components/checkbox'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
import { Checkbox } from '@evobgp/ui/components/checkbox'
|
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { SelectField } from '@/components/select-field'
|
import { SelectField } from '@/components/select-field'
|
||||||
import { useCreatePeerMutation, useUpdatePeerMutation } from '@/queries/network'
|
import { useCreatePeerMutation, useUpdatePeerMutation } from '@/queries/network'
|
||||||
@@ -102,13 +95,23 @@ export function PeerFormDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-sm">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>{editTarget ? 'Редактировать пира' : 'Новый пир'}</DialogTitle>
|
title={editTarget ? 'Редактировать пира' : 'Новый пир'}
|
||||||
<DialogDescription>BGP-сосед для установки сессии</DialogDescription>
|
description="BGP-сосед для установки сессии"
|
||||||
</DialogHeader>
|
className="sm:max-w-sm"
|
||||||
<div className="flex flex-col gap-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<LoadingButton type="button" loading={saving} onClick={save}>
|
||||||
|
{editTarget ? 'Сохранить' : 'Создать'}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="peer-name">Имя пира (опционально)</Label>
|
<Label htmlFor="peer-name">Имя пира (опционально)</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -160,16 +163,6 @@ export function PeerFormDialog({
|
|||||||
onCheckedChange={(v) => setEnabled(v === true)}
|
onCheckedChange={(v) => setEnabled(v === true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</Button>
|
|
||||||
<LoadingButton type="button" loading={saving} onClick={save}>
|
|
||||||
{editTarget ? 'Сохранить' : 'Создать'}
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,10 @@ import { useEffect, useState } from 'react'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@evobgp/ui/components/dialog'
|
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { SelectField } from '@/components/select-field'
|
import { SelectField } from '@/components/select-field'
|
||||||
import { useCreateSpeakerMutation } from '@/queries/network'
|
import { useCreateSpeakerMutation } from '@/queries/network'
|
||||||
@@ -98,13 +91,23 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<FormDrawer
|
||||||
<DialogContent className="sm:max-w-md">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle>Новый спикер</DialogTitle>
|
title="Новый спикер"
|
||||||
<DialogDescription>BIRD-агент на ноде реплики или control plane</DialogDescription>
|
description="BIRD-агент на ноде реплики или control plane"
|
||||||
</DialogHeader>
|
className="sm:max-w-md"
|
||||||
<div className="flex flex-col gap-4 py-2">
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<LoadingButton type="button" loading={createMutation.isPending} onClick={save}>
|
||||||
|
Создать
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="speaker-endpoint">Endpoint</Label>
|
<Label htmlFor="speaker-endpoint">Endpoint</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -154,16 +157,6 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormDrawer>
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
|
||||||
Отмена
|
|
||||||
</Button>
|
|
||||||
<LoadingButton type="button" loading={createMutation.isPending} onClick={save}>
|
|
||||||
Создать
|
|
||||||
</LoadingButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,226 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer"
|
||||||
|
|
||||||
|
import { cn } from "@evobgp/ui/lib/utils"
|
||||||
|
|
||||||
|
type DrawerContextProps = {
|
||||||
|
hasSnapPoints: boolean
|
||||||
|
modal: DrawerPrimitive.Root.Props["modal"]
|
||||||
|
showSwipeHandle: boolean
|
||||||
|
swipeDirection: NonNullable<DrawerPrimitive.Root.Props["swipeDirection"]>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DrawerContext = React.createContext<DrawerContextProps | null>(null)
|
||||||
|
|
||||||
|
function useDrawer() {
|
||||||
|
const context = React.useContext(DrawerContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useDrawer must be used within a Drawer.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
modal = true,
|
||||||
|
showSwipeHandle = false,
|
||||||
|
snapPoints,
|
||||||
|
swipeDirection = "down",
|
||||||
|
...props
|
||||||
|
}: DrawerPrimitive.Root.Props & {
|
||||||
|
showSwipeHandle?: boolean
|
||||||
|
}) {
|
||||||
|
const hasSnapPoints = snapPoints != null && snapPoints.length > 0
|
||||||
|
const contextValue = React.useMemo(
|
||||||
|
() => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),
|
||||||
|
[hasSnapPoints, modal, showSwipeHandle, swipeDirection]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerContext.Provider value={contextValue}>
|
||||||
|
<DrawerPrimitive.Root
|
||||||
|
data-slot="drawer"
|
||||||
|
modal={modal}
|
||||||
|
snapPoints={snapPoints}
|
||||||
|
swipeDirection={swipeDirection}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DrawerContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DrawerPrimitive.Backdrop.Props) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Backdrop
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 min-h-dvh bg-black/10 opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-snap-points:[--drawer-overlay-min-opacity:0.5] data-starting-style:opacity-0 data-swiping:duration-0 supports-backdrop-filter:backdrop-blur-xs supports-[-webkit-touch-callout:none]:absolute",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerSwipeHandle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-swipe-handle"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 flex shrink-0 cursor-grab transition-opacity duration-200 group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-[swipe-axis=x]/drawer-popup:h-full group-data-[swipe-axis=x]/drawer-popup:w-3 group-data-[swipe-axis=x]/drawer-popup:items-center group-data-[swipe-axis=y]/drawer-popup:h-3 group-data-[swipe-axis=y]/drawer-popup:w-full group-data-[swipe-axis=y]/drawer-popup:justify-center group-data-[swipe-direction=down]/drawer-popup:items-end group-data-[swipe-direction=left]/drawer-popup:order-last group-data-[swipe-direction=left]/drawer-popup:justify-start group-data-[swipe-direction=right]/drawer-popup:justify-end group-data-[swipe-direction=up]/drawer-popup:order-last group-data-[swipe-direction=up]/drawer-popup:items-start after:block after:shrink-0 after:rounded-full after:bg-muted group-data-[swipe-axis=x]/drawer-popup:after:h-24 group-data-[swipe-axis=x]/drawer-popup:after:w-1 group-data-[swipe-axis=y]/drawer-popup:after:h-1 group-data-[swipe-axis=y]/drawer-popup:after:w-24 active:cursor-grabbing",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: DrawerPrimitive.Popup.Props) {
|
||||||
|
const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer()
|
||||||
|
const swipeAxis =
|
||||||
|
swipeDirection === "down" || swipeDirection === "up" ? "y" : "x"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
{modal === true && (
|
||||||
|
<DrawerOverlay data-snap-points={hasSnapPoints ? "" : undefined} />
|
||||||
|
)}
|
||||||
|
<DrawerPrimitive.Viewport
|
||||||
|
data-slot="drawer-viewport"
|
||||||
|
data-modal={modal}
|
||||||
|
className="pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto"
|
||||||
|
>
|
||||||
|
<DrawerPrimitive.Popup
|
||||||
|
data-slot="drawer-popup"
|
||||||
|
data-swipe-axis={swipeAxis}
|
||||||
|
data-snap-points={hasSnapPoints ? "" : undefined}
|
||||||
|
className={cn(
|
||||||
|
// Base.
|
||||||
|
"group/drawer-popup pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] flex-col bg-popover text-sm text-popover-foreground transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none select-none [interpolate-size:allow-keywords] data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=down]:border-t data-[swipe-direction=left]:rounded-r-xl data-[swipe-direction=left]:border-r data-[swipe-direction=right]:rounded-l-xl data-[swipe-direction=right]:border-l data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=up]:border-b",
|
||||||
|
// Nested.
|
||||||
|
"data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95",
|
||||||
|
// Bleed.
|
||||||
|
"after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=x]:after:inset-y-0 data-[swipe-axis=x]:after:w-(--bleed) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:right-full data-[swipe-direction=right]:after:left-full data-[swipe-direction=up]:after:bottom-full",
|
||||||
|
// Sizing.
|
||||||
|
"[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:sm:[--drawer-content-width:24rem]",
|
||||||
|
// Stack.
|
||||||
|
"[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]",
|
||||||
|
// Transitions.
|
||||||
|
"data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]",
|
||||||
|
// Axis: y.
|
||||||
|
"data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)",
|
||||||
|
// Axis: x.
|
||||||
|
"data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row",
|
||||||
|
// Direction: down.
|
||||||
|
"data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]",
|
||||||
|
// Direction: up.
|
||||||
|
"data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]",
|
||||||
|
// Direction: left.
|
||||||
|
"data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]",
|
||||||
|
// Direction: right.
|
||||||
|
"data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showSwipeHandle && <DrawerSwipeHandle />}
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] select-text group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-swiping/drawer-popup:select-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPrimitive.Popup>
|
||||||
|
</DrawerPrimitive.Viewport>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn(
|
||||||
|
"flex shrink-0 flex-col gap-0.5 p-4 pb-0 group-data-[swipe-axis=y]/drawer-popup:text-center md:gap-0.5 md:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex shrink-0 flex-col gap-2 p-4 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn(
|
||||||
|
"text-base font-medium text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DrawerPrimitive.Description.Props) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-sm text-balance text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerSwipeHandle,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user