From c265c06f937e1b44f79efa237381ce8d9da3963e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Jul 2026 14:09:40 +0700 Subject: [PATCH] refactor: replace dialog components with FormDrawer for improved UI consistency 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. --- .../access/api-key-create-dialog.tsx | 82 +++---- apps/web/src/components/confirm-dialog.tsx | 133 ++++++++--- .../firewall/firewall-rule-create-dialog.tsx | 82 +++---- apps/web/src/components/form-drawer.tsx | 47 ++++ .../modules/module-as-entry-dialog.tsx | 75 +++--- .../modules/module-cdn-source-dialog.tsx | 205 ++++++++-------- .../modules/module-domain-entry-dialog.tsx | 64 +++-- .../modules/module-entries-section.tsx | 40 +--- .../modules/module-ip-range-entry-dialog.tsx | 64 +++-- .../components/network/peer-form-dialog.tsx | 137 +++++------ .../network/speaker-form-dialog.tsx | 131 +++++----- apps/web/tsconfig.tsbuildinfo | 2 +- packages/ui/src/components/drawer.tsx | 226 ++++++++++++++++++ 13 files changed, 782 insertions(+), 506 deletions(-) create mode 100644 apps/web/src/components/form-drawer.tsx create mode 100644 packages/ui/src/components/drawer.tsx diff --git a/apps/web/src/components/access/api-key-create-dialog.tsx b/apps/web/src/components/access/api-key-create-dialog.tsx index 750ec86..47ed82c 100644 --- a/apps/web/src/components/access/api-key-create-dialog.tsx +++ b/apps/web/src/components/access/api-key-create-dialog.tsx @@ -2,16 +2,10 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' 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 { Label } from '@evobgp/ui/components/label' +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { SelectField } from '@/components/select-field' import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels' @@ -68,48 +62,48 @@ export function ApiKeyCreateDialog({ open, onOpenChange, onCreated }: ApiKeyCrea } return ( - - - - Новый API-ключ - -
-
- - setName(e.target.value)} - placeholder="CI / оператор UI" - /> -
- v && setRole(v as ApiKeyRole)} - /> -
- - setExpiresLocal(e.target.value)} - /> -
-
- + Создать - -
-
+ + } + > +
+ + setName(e.target.value)} + placeholder="CI / оператор UI" + /> +
+ v && setRole(v as ApiKeyRole)} + /> +
+ + setExpiresLocal(e.target.value)} + /> +
+ ) } diff --git a/apps/web/src/components/confirm-dialog.tsx b/apps/web/src/components/confirm-dialog.tsx index 76b3860..87acb56 100644 --- a/apps/web/src/components/confirm-dialog.tsx +++ b/apps/web/src/components/confirm-dialog.tsx @@ -1,53 +1,126 @@ +import { Button } from '@evobgp/ui/components/button' import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@evobgp/ui/components/alert-dialog' + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@evobgp/ui/components/drawer' import type { ReactElement, ReactNode } from 'react' -interface ConfirmDialogProps { - trigger: ReactElement +type ConfirmDialogBaseProps = { title: string description?: ReactNode confirmLabel?: string cancelLabel?: string destructive?: boolean onConfirm: () => void + confirmDisabled?: boolean + confirmLoading?: boolean + confirmLoadingLabel?: string } -export function ConfirmDialog({ - trigger, +type ConfirmDialogWithTrigger = ConfirmDialogBaseProps & { + trigger: ReactElement + open?: never + onOpenChange?: never +} + +type ConfirmDialogControlled = ConfirmDialogBaseProps & { + trigger?: never + open: boolean + onOpenChange: (open: boolean) => void +} + +type ConfirmDialogProps = ConfirmDialogWithTrigger | ConfirmDialogControlled + +function ConfirmDrawerBody({ title, description, confirmLabel = 'Подтвердить', cancelLabel = 'Отмена', destructive, onConfirm, -}: ConfirmDialogProps) { + confirmDisabled, + confirmLoading, + confirmLoadingLabel, + controlled, +}: ConfirmDialogBaseProps & { controlled?: boolean }) { + const confirmText = + confirmLoading && confirmLoadingLabel + ? confirmLoadingLabel + : confirmLoading + ? `${confirmLabel}…` + : confirmLabel + return ( - - - - - {title} - {description ? {description} : null} - - - {cancelLabel} - + + {title} + {description ? {description} : null} + + + }> + {cancelLabel} + + {controlled ? ( + + ) : ( + + } + onClick={onConfirm} + > + {confirmText} + + )} + + + ) +} + +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 ( + + + + + + + ) + } + + return ( + + + + + ) } diff --git a/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx b/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx index 2cd7afe..e56d036 100644 --- a/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx +++ b/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx @@ -1,16 +1,10 @@ import { useEffect, useState } from 'react' 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 { Label } from '@evobgp/ui/components/label' +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { CommunitySelect } from '@/components/modules/community-select' import { SelectField } from '@/components/select-field' @@ -60,48 +54,48 @@ export function FirewallRuleCreateDialog({ } return ( - - - - Новое правило - -
- v && setAction(v as 'block' | 'accept')} - /> - -
- - setComment(e.target.value)} - /> -
-
- + Добавить - -
-
+ + } + > + v && setAction(v as 'block' | 'accept')} + /> + +
+ + setComment(e.target.value)} + /> +
+ ) } diff --git a/apps/web/src/components/form-drawer.tsx b/apps/web/src/components/form-drawer.tsx new file mode 100644 index 0000000..9fbd67c --- /dev/null +++ b/apps/web/src/components/form-drawer.tsx @@ -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 ( + + + + {title} + {description ? {description} : null} + + +
{children}
+
+ {footer} +
+
+ ) +} diff --git a/apps/web/src/components/modules/module-as-entry-dialog.tsx b/apps/web/src/components/modules/module-as-entry-dialog.tsx index 45cb21b..ed989fb 100644 --- a/apps/web/src/components/modules/module-as-entry-dialog.tsx +++ b/apps/web/src/components/modules/module-as-entry-dialog.tsx @@ -1,17 +1,10 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@evobgp/ui/components/dialog' import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { CommunitySelect } from '@/components/modules/community-select' import { ApiError, apiMutate } from '@/lib/api-client' @@ -72,45 +65,43 @@ export function ModuleAsEntryDialog({ } return ( - - - - {edit ? 'Редактировать запись' : 'Новая AS-запись'} - - Номер автономной системы и community для политики анонса. - - -
-
- - setForm((s) => ({ ...s, asn: Number(e.target.value) }))} - /> -
- setForm((s) => ({ ...s, community_id: v }))} - communities={communities} - nullable - /> -
- + onOpenChange(false)}> Отмена void save()}> {edit ? 'Сохранить' : 'Добавить'} - -
-
+ + } + > +
+ + setForm((s) => ({ ...s, asn: Number(e.target.value) }))} + /> +
+ setForm((s) => ({ ...s, community_id: v }))} + communities={communities} + nullable + /> + ) } diff --git a/apps/web/src/components/modules/module-cdn-source-dialog.tsx b/apps/web/src/components/modules/module-cdn-source-dialog.tsx index d607417..6774f84 100644 --- a/apps/web/src/components/modules/module-cdn-source-dialog.tsx +++ b/apps/web/src/components/modules/module-cdn-source-dialog.tsx @@ -1,21 +1,14 @@ import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@evobgp/ui/components/dialog' +import { Button } from '@evobgp/ui/components/button' import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' -import { SelectField } from '@/components/select-field' -import { Button } from '@evobgp/ui/components/button' - +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { CommunitySelect } from '@/components/modules/community-select' +import { SelectField } from '@/components/select-field' import { ApiError, apiMutate } from '@/lib/api-client' import { normalizeCdnSourceKind } from '@/lib/modules/helpers' import type { BgpCommunity, CdnPreviewResponse, CdnSource, CdnSourceCreate } from '@/types/api' @@ -154,107 +147,107 @@ export function ModuleCdnSourceDialog({ } return ( - - - - {edit ? 'Редактировать источник' : 'Новый CDN-источник'} - -
-
- - setForm((s) => ({ ...s, url: e.target.value }))} - /> -
- v && setForm((s) => ({ ...s, source_kind: v }))} - /> -
- - setForm((s) => ({ ...s, prefix_path: e.target.value }))} - /> - {form.source_kind === 'json' && !form.prefix_path?.trim() ? ( -

- Для JSON укажите путь к полям с CIDR; пустой путь может не дать префиксов. -

- ) : null} -
- setForm((s) => ({ ...s, community_id: v }))} - communities={communities} - nullable - /> -
- - - setForm((s) => ({ - ...s, - refresh_interval_sec: e.target.value ? Number(e.target.value) : null, - })) - } - /> -
-
-
- - {previewError ? ( - {previewError} - ) : previewOk ? ( - - Всего: {previewTotal} - {previewTruncated ? ( - (обрезано) - ) : null} - - ) : null} -
- {previewItems.length > 0 ? ( -
    - {previewItems.map((item, i) => ( -
  • - {item} -
  • - ))} -
- ) : null} -
-
- + onOpenChange(false)}> Отмена void save()}> {edit ? 'Сохранить' : 'Добавить'} - -
-
+ + } + > +
+ + setForm((s) => ({ ...s, url: e.target.value }))} + /> +
+ v && setForm((s) => ({ ...s, source_kind: v }))} + /> +
+ + setForm((s) => ({ ...s, prefix_path: e.target.value }))} + /> + {form.source_kind === 'json' && !form.prefix_path?.trim() ? ( +

+ Для JSON укажите путь к полям с CIDR; пустой путь может не дать префиксов. +

+ ) : null} +
+ setForm((s) => ({ ...s, community_id: v }))} + communities={communities} + nullable + /> +
+ + + setForm((s) => ({ + ...s, + refresh_interval_sec: e.target.value ? Number(e.target.value) : null, + })) + } + /> +
+
+
+ + {previewError ? ( + {previewError} + ) : previewOk ? ( + + Всего: {previewTotal} + {previewTruncated ? ( + (обрезано) + ) : null} + + ) : null} +
+ {previewItems.length > 0 ? ( +
    + {previewItems.map((item, i) => ( +
  • + {item} +
  • + ))} +
+ ) : null} +
+ ) } diff --git a/apps/web/src/components/modules/module-domain-entry-dialog.tsx b/apps/web/src/components/modules/module-domain-entry-dialog.tsx index c6bc8e7..5f2bbe9 100644 --- a/apps/web/src/components/modules/module-domain-entry-dialog.tsx +++ b/apps/web/src/components/modules/module-domain-entry-dialog.tsx @@ -1,16 +1,10 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@evobgp/ui/components/dialog' import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { CommunitySelect } from '@/components/modules/community-select' import { ApiError, apiMutate } from '@/lib/api-client' @@ -70,39 +64,39 @@ export function ModuleDomainEntryDialog({ } return ( - - - - {edit ? 'Редактировать домен' : 'Новый домен'} - -
-
- - setForm((s) => ({ ...s, fqdn: e.target.value }))} - /> -
- setForm((s) => ({ ...s, community_id: v }))} - communities={communities} - nullable - /> -
- + onOpenChange(false)}> Отмена void save()}> {edit ? 'Сохранить' : 'Добавить'} - -
-
+ + } + > +
+ + setForm((s) => ({ ...s, fqdn: e.target.value }))} + /> +
+ setForm((s) => ({ ...s, community_id: v }))} + communities={communities} + nullable + /> + ) } diff --git a/apps/web/src/components/modules/module-entries-section.tsx b/apps/web/src/components/modules/module-entries-section.tsx index d03b5f6..c2dbc11 100644 --- a/apps/web/src/components/modules/module-entries-section.tsx +++ b/apps/web/src/components/modules/module-entries-section.tsx @@ -2,18 +2,9 @@ import { useState } from 'react' import { Plus } from 'lucide-react' 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 { ConfirmDialog } from '@/components/confirm-dialog' import { DataGridCard } from '@/components/data-grid-shell' import { QueryState } from '@/components/query-state' import { TableSkeleton } from '@/components/skeletons' @@ -218,24 +209,17 @@ export function ModuleEntriesSection({ /> ) : null} - !open && setDeleteTarget(null)}> - - - Удалить запись? - {deleteDescription(deleteTarget)} - - - Отмена - void confirmDelete()} - > - {deleting ? 'Удаление…' : 'Удалить'} - - - - + !open && setDeleteTarget(null)} + title="Удалить запись?" + description={deleteDescription(deleteTarget)} + confirmLabel="Удалить" + confirmLoadingLabel="Удаление…" + destructive + confirmLoading={deleting} + onConfirm={() => void confirmDelete()} + /> ) } diff --git a/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx b/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx index 08ee885..c54887b 100644 --- a/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx +++ b/apps/web/src/components/modules/module-ip-range-entry-dialog.tsx @@ -1,16 +1,10 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@evobgp/ui/components/dialog' import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { CommunitySelect } from '@/components/modules/community-select' import { ApiError, apiMutate } from '@/lib/api-client' @@ -70,39 +64,39 @@ export function ModuleIpRangeEntryDialog({ } return ( - - - - {edit ? 'Редактировать диапазон' : 'Новый IP-диапазон'} - -
-
- - setForm((s) => ({ ...s, prefix: e.target.value }))} - /> -
- setForm((s) => ({ ...s, community_id: v ?? '' }))} - communities={communities} - placeholder="Выберите community" - /> -
- + onOpenChange(false)}> Отмена void save()}> {edit ? 'Сохранить' : 'Добавить'} - -
-
+ + } + > +
+ + setForm((s) => ({ ...s, prefix: e.target.value }))} + /> +
+ setForm((s) => ({ ...s, community_id: v ?? '' }))} + communities={communities} + placeholder="Выберите community" + /> + ) } diff --git a/apps/web/src/components/network/peer-form-dialog.tsx b/apps/web/src/components/network/peer-form-dialog.tsx index f89ff2c..09786b1 100644 --- a/apps/web/src/components/network/peer-form-dialog.tsx +++ b/apps/web/src/components/network/peer-form-dialog.tsx @@ -2,18 +2,11 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' import { Button } from '@evobgp/ui/components/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@evobgp/ui/components/dialog' +import { Checkbox } from '@evobgp/ui/components/checkbox' import { Input } from '@evobgp/ui/components/input' 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 { SelectField } from '@/components/select-field' import { useCreatePeerMutation, useUpdatePeerMutation } from '@/queries/network' @@ -102,74 +95,74 @@ export function PeerFormDialog({ } return ( - - - - {editTarget ? 'Редактировать пира' : 'Новый пир'} - BGP-сосед для установки сессии - -
-
- - setName(e.target.value)} - /> -
-
- - setNeighbor(e.target.value)} - required - /> -
-
- - setRemoteAsn(e.target.value)} - required - /> -
- setBgpSpeakerId(v || null)} - placeholder="Все спикеры" - /> -
-
- -

- Выключенный пир не попадает в конфиг BIRD до следующей ревизии. -

-
- setEnabled(v === true)} - /> -
-
- + {editTarget ? 'Сохранить' : 'Создать'} - -
-
+ + } + > +
+ + setName(e.target.value)} + /> +
+
+ + setNeighbor(e.target.value)} + required + /> +
+
+ + setRemoteAsn(e.target.value)} + required + /> +
+ setBgpSpeakerId(v || null)} + placeholder="Все спикеры" + /> +
+
+ +

+ Выключенный пир не попадает в конфиг BIRD до следующей ревизии. +

+
+ setEnabled(v === true)} + /> +
+ ) } diff --git a/apps/web/src/components/network/speaker-form-dialog.tsx b/apps/web/src/components/network/speaker-form-dialog.tsx index 1db5689..56010ac 100644 --- a/apps/web/src/components/network/speaker-form-dialog.tsx +++ b/apps/web/src/components/network/speaker-form-dialog.tsx @@ -2,17 +2,10 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' 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 { Label } from '@evobgp/ui/components/label' +import { FormDrawer } from '@/components/form-drawer' import { LoadingButton } from '@/components/loading-button' import { SelectField } from '@/components/select-field' import { useCreateSpeakerMutation } from '@/queries/network' @@ -98,72 +91,72 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps } return ( - - - - Новый спикер - BIRD-агент на ноде реплики или control plane - -
-
- - handleEndpointChange(e.target.value)} - /> -
- setRole(v ?? 'replica')} - /> -
- - setAgentDomain(e.target.value)} - /> -
-
- - handleNodeIpv4Change(e.target.value)} - /> -
-
- - { - setBgpSourceManual(true) - setBgpSourceIpv4(e.target.value) - }} - /> -
-
- + Создать - -
-
+ + } + > +
+ + handleEndpointChange(e.target.value)} + /> +
+ setRole(v ?? 'replica')} + /> +
+ + setAgentDomain(e.target.value)} + /> +
+
+ + handleNodeIpv4Change(e.target.value)} + /> +
+
+ + { + setBgpSourceManual(true) + setBgpSourceIpv4(e.target.value) + }} + /> +
+ ) } diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index 17f5ace..f58e79f 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/form-drawer.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/packages/ui/src/components/drawer.tsx b/packages/ui/src/components/drawer.tsx new file mode 100644 index 0000000..ffe1f0b --- /dev/null +++ b/packages/ui/src/components/drawer.tsx @@ -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 +} + +const DrawerContext = React.createContext(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 ( + + + + ) +} + +function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) { + return +} + +function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) { + return +} + +function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) { + return +} + +function DrawerOverlay({ + className, + ...props +}: DrawerPrimitive.Backdrop.Props) { + return ( + + ) +} + +function DrawerSwipeHandle({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +