style(ui): unify modal dialogs with SidebarDialog component
Replace ModalFrame and related modal patterns in settings and upload dialogs with the new SidebarDialog component to establish a consistent sidebar-based dialog layout. Refactor SettingsModal and UploadMenuDialog to use SidebarDialog, updating header, navigation, and content handling accordingly. Update DocumentList to use DocumentUploader in compact mode for sidebar upload action. Export SidebarDialog from the UI module. Minor fixes in web-loader for IP parsing and option naming. This change standardizes dialog UI structure and improves maintainability.
This commit is contained in:
parent
5a44a59ed1
commit
d86df06f9b
8 changed files with 402 additions and 364 deletions
|
|
@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { requireAuthContext } from '@/lib/server/auth/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { fetchAndParseUrl } from '@/lib/server/documents/web-loader';
|
import { fetchAndParseUrl } from '@/lib/server/documents/web-loader';
|
||||||
import { errorToLog, serverLogger } from '@/lib/server/logger';
|
import { errorToLog, serverLogger } from '@/lib/server/logger';
|
||||||
import { errorResponse } from '@/lib/server/errors/next-response';
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import { flushUserPreferencesSync } from '@/lib/client/api/user-state';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useLibraryDocumentsQuery } from '@/hooks/useLibraryDocumentsQuery';
|
import { useLibraryDocumentsQuery } from '@/hooks/useLibraryDocumentsQuery';
|
||||||
import {
|
import {
|
||||||
SidebarNav,
|
SidebarDialog,
|
||||||
SidebarNavItem,
|
SidebarNavItem,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -50,8 +50,6 @@ import {
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
ModalFrame,
|
|
||||||
ModalTitle,
|
|
||||||
Select,
|
Select,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
@ -562,17 +560,15 @@ export function SettingsModal({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalFrame
|
<SidebarDialog
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onClose={resetToCurrent}
|
onClose={resetToCurrent}
|
||||||
size="xl"
|
size="xl"
|
||||||
panelTestId="settings-modal"
|
panelTestId="settings-modal"
|
||||||
className={isChangelogOpen ? 'z-[90]' : 'z-50'}
|
modalClassName={isChangelogOpen ? 'z-[90]' : 'z-50'}
|
||||||
>
|
headerTitle={
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between px-5 py-3 border-b border-line-soft">
|
|
||||||
<div className="flex items-baseline gap-4">
|
<div className="flex items-baseline gap-4">
|
||||||
<ModalTitle>Settings</ModalTitle>
|
<span>Settings</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -582,7 +578,8 @@ export function SettingsModal({
|
||||||
{displayVersion ? `v${displayVersion} · Changelog` : 'Changelog'}
|
{displayVersion ? `v${displayVersion} · Changelog` : 'Changelog'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
}
|
||||||
|
headerRight={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -591,61 +588,27 @@ export function SettingsModal({
|
||||||
>
|
>
|
||||||
Privacy
|
Privacy
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
</div>
|
showCloseButton={false}
|
||||||
|
sections={visibleSections}
|
||||||
{isChangelogOpen ? (
|
activeSectionId={activeSection}
|
||||||
|
onSectionChange={setActiveSection}
|
||||||
|
className="h-[490px]"
|
||||||
|
contentClassName={
|
||||||
|
activeSection === 'admin'
|
||||||
|
? 'bg-[radial-gradient(circle_at_top_right,color-mix(in_srgb,var(--accent),transparent_92%),transparent_35%)]'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
customContent={
|
||||||
|
isChangelogOpen ? (
|
||||||
<SettingsChangelogPanel
|
<SettingsChangelogPanel
|
||||||
appVersion={runtimeConfig.appVersion}
|
appVersion={runtimeConfig.appVersion}
|
||||||
manifestUrl={runtimeConfig.changelogFeedUrl}
|
manifestUrl={runtimeConfig.changelogFeedUrl}
|
||||||
onClose={() => setIsChangelogOpen(false)}
|
onClose={() => setIsChangelogOpen(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : undefined
|
||||||
<>
|
}
|
||||||
{/* Mobile nav */}
|
>
|
||||||
<SidebarNav layout="grid" className="sm:hidden border-b border-line-soft bg-background p-2">
|
|
||||||
{visibleSections.map((section) => {
|
|
||||||
const Icon = section.icon;
|
|
||||||
return (
|
|
||||||
<SidebarNavItem
|
|
||||||
compact
|
|
||||||
key={section.id}
|
|
||||||
onClick={() => setActiveSection(section.id)}
|
|
||||||
active={activeSection === section.id}
|
|
||||||
icon={<Icon className="w-3.5 h-3.5" />}
|
|
||||||
label={section.label}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SidebarNav>
|
|
||||||
|
|
||||||
<div className="flex flex-row h-[490px]">
|
|
||||||
{/* Desktop: vertical sidebar */}
|
|
||||||
<nav className="hidden sm:block w-44 shrink-0 border-r border-line-soft bg-background p-2">
|
|
||||||
<SidebarNav>
|
|
||||||
{visibleSections.map((section) => {
|
|
||||||
const Icon = section.icon;
|
|
||||||
const active = activeSection === section.id;
|
|
||||||
return (
|
|
||||||
<SidebarNavItem
|
|
||||||
key={section.id}
|
|
||||||
onClick={() => setActiveSection(section.id)}
|
|
||||||
active={active}
|
|
||||||
icon={<Icon className="w-4 h-4" />}
|
|
||||||
label={section.label}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SidebarNav>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className={`flex-1 min-w-0 p-3 overflow-y-auto ${
|
|
||||||
activeSection === 'admin'
|
|
||||||
? 'bg-[radial-gradient(circle_at_top_right,color-mix(in_srgb,var(--accent),transparent_92%),transparent_35%)]'
|
|
||||||
: ''
|
|
||||||
}`}>
|
|
||||||
{/* API Section */}
|
{/* API Section */}
|
||||||
{activeSection === 'api' && (
|
{activeSection === 'api' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -1174,11 +1137,7 @@ export function SettingsModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SidebarDialog>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ModalFrame>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={showDeleteAccountConfirm}
|
isOpen={showDeleteAccountConfirm}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||||
import { CreateFolderDialog } from '@/components/doclist/CreateFolderDialog';
|
import { CreateFolderDialog } from '@/components/doclist/CreateFolderDialog';
|
||||||
import { DocumentListSkeleton } from '@/components/doclist/DocumentListSkeleton';
|
import { DocumentListSkeleton } from '@/components/doclist/DocumentListSkeleton';
|
||||||
import { DocumentUploader, type UploadBatchState } from '@/components/documents/DocumentUploader';
|
import { DocumentUploader, type UploadBatchState } from '@/components/documents/DocumentUploader';
|
||||||
import { IconButton, SidebarNavItem } from '@/components/ui';
|
import { IconButton } from '@/components/ui';
|
||||||
import { UploadIcon } from '@/components/icons/Icons';
|
|
||||||
import { UploadMenuDialog } from '@/components/documents/UploadMenuDialog';
|
import { UploadMenuDialog } from '@/components/documents/UploadMenuDialog';
|
||||||
import { DocumentDndProvider } from './dnd/DocumentDndProvider';
|
import { DocumentDndProvider } from './dnd/DocumentDndProvider';
|
||||||
import {
|
import {
|
||||||
|
|
@ -623,11 +622,10 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
width={sidebarWidth}
|
width={sidebarWidth}
|
||||||
onWidthChange={setSidebarWidth}
|
onWidthChange={setSidebarWidth}
|
||||||
topSlot={(
|
topSlot={(
|
||||||
<SidebarNavItem
|
<DocumentUploader
|
||||||
compact
|
variant="compact"
|
||||||
|
onUploadBatchChange={handleUploadBatchChange}
|
||||||
onClick={() => setIsUploadDialogOpen(true)}
|
onClick={() => setIsUploadDialogOpen(true)}
|
||||||
icon={<UploadIcon className="w-3.5 h-3.5" />}
|
|
||||||
label="Upload documents"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
bottomSlot={(
|
bottomSlot={(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface DocumentUploaderProps {
|
||||||
variant?: 'default' | 'compact' | 'overlay';
|
variant?: 'default' | 'compact' | 'overlay';
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
onUploadBatchChange?: (state: UploadBatchState) => void;
|
onUploadBatchChange?: (state: UploadBatchState) => void;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadBatchState {
|
export interface UploadBatchState {
|
||||||
|
|
@ -28,6 +29,7 @@ export function DocumentUploader({
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
children,
|
children,
|
||||||
onUploadBatchChange,
|
onUploadBatchChange,
|
||||||
|
onClick,
|
||||||
}: DocumentUploaderProps) {
|
}: DocumentUploaderProps) {
|
||||||
const uploaderId = useId();
|
const uploaderId = useId();
|
||||||
const enableDocx = useFeatureFlag('enableDocxConversion');
|
const enableDocx = useFeatureFlag('enableDocxConversion');
|
||||||
|
|
@ -88,8 +90,8 @@ export function DocumentUploader({
|
||||||
},
|
},
|
||||||
multiple: true,
|
multiple: true,
|
||||||
disabled: isUploading,
|
disabled: isUploading,
|
||||||
noClick: variant === 'overlay',
|
noClick: variant === 'overlay' || !!onClick,
|
||||||
noKeyboard: variant === 'overlay'
|
noKeyboard: variant === 'overlay' || !!onClick
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDisabled = isUploading;
|
const isDisabled = isUploading;
|
||||||
|
|
@ -132,6 +134,12 @@ export function DocumentUploader({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (onClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={dropzoneSurfaceClass({
|
className={dropzoneSurfaceClass({
|
||||||
variant: variant === 'compact' ? 'compact' : 'default',
|
variant: variant === 'compact' ? 'compact' : 'default',
|
||||||
active: isDragActive,
|
active: isDragActive,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ import toast from 'react-hot-toast';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { importUrl } from '@/lib/client/api/documents';
|
import { importUrl } from '@/lib/client/api/documents';
|
||||||
import {
|
import {
|
||||||
ModalFrame,
|
SidebarDialog,
|
||||||
ModalTitle,
|
|
||||||
SidebarNav,
|
|
||||||
SidebarNavItem,
|
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Input,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
|
|
@ -163,64 +160,15 @@ export function UploadMenuDialog({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalFrame open={isOpen} onClose={handleClose} size="xl">
|
<SidebarDialog
|
||||||
<div className="flex flex-col h-[480px]">
|
open={isOpen}
|
||||||
{/* Header */}
|
onClose={handleClose}
|
||||||
<div className="flex items-center justify-between px-5 py-3 border-b border-line-soft shrink-0">
|
headerTitle="Add Documents"
|
||||||
<ModalTitle>Add Documents</ModalTitle>
|
sections={SIDEBAR_SECTIONS}
|
||||||
<button
|
activeSectionId={activeTab}
|
||||||
onClick={handleClose}
|
onSectionChange={handleTabChange}
|
||||||
className="rounded-md p-1 text-soft hover:bg-accent-wash hover:text-foreground transition-colors duration-base"
|
className="h-[480px]"
|
||||||
aria-label="Close dialog"
|
|
||||||
>
|
>
|
||||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Navigation (Horizontal row of items) */}
|
|
||||||
<SidebarNav layout="grid" className="sm:hidden border-b border-line-soft bg-background p-2 shrink-0">
|
|
||||||
{SIDEBAR_SECTIONS.map((section) => {
|
|
||||||
const Icon = section.icon;
|
|
||||||
return (
|
|
||||||
<SidebarNavItem
|
|
||||||
compact
|
|
||||||
key={section.id}
|
|
||||||
onClick={() => handleTabChange(section.id)}
|
|
||||||
active={activeTab === section.id}
|
|
||||||
icon={<Icon className="w-3.5 h-3.5" />}
|
|
||||||
label={section.label}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SidebarNav>
|
|
||||||
|
|
||||||
{/* Main Body (Split layout on desktop) */}
|
|
||||||
<div className="flex flex-row min-h-0 flex-1">
|
|
||||||
{/* Desktop Navigation (Left sidebar) */}
|
|
||||||
<nav className="hidden sm:block w-fit shrink-0 border-r border-line-soft bg-background p-2">
|
|
||||||
<SidebarNav>
|
|
||||||
{SIDEBAR_SECTIONS.map((section) => {
|
|
||||||
const Icon = section.icon;
|
|
||||||
const active = activeTab === section.id;
|
|
||||||
return (
|
|
||||||
<SidebarNavItem
|
|
||||||
key={section.id}
|
|
||||||
onClick={() => handleTabChange(section.id)}
|
|
||||||
active={active}
|
|
||||||
icon={<Icon className="w-4 h-4" />}
|
|
||||||
label={section.label}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SidebarNav>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Tab Panel Content (Right detail view) */}
|
|
||||||
<div className="flex-1 min-w-0 p-4 overflow-y-auto bg-surface">
|
|
||||||
|
|
||||||
{/* TAB 1: File Uploader */}
|
{/* TAB 1: File Uploader */}
|
||||||
{activeTab === 'file' && (
|
{activeTab === 'file' && (
|
||||||
<div className="h-full flex flex-col gap-4 animate-fade-in">
|
<div className="h-full flex flex-col gap-4 animate-fade-in">
|
||||||
|
|
@ -352,7 +300,7 @@ export function UploadMenuDialog({
|
||||||
|
|
||||||
{/* Progress / Loading Stepper */}
|
{/* Progress / Loading Stepper */}
|
||||||
{importStep !== 'idle' && (
|
{importStep !== 'idle' && (
|
||||||
<div className="rounded-lg border border-line bg-surface-sunken p-4 flex flex-col gap-3.5 transition-all duration-base">
|
<div className="rounded-lg border border-line bg-surface-sunken p-4 flex flex-col gap-3.5 transition-colors duration-base">
|
||||||
{importStep === 'error' ? (
|
{importStep === 'error' ? (
|
||||||
<div className="flex items-start gap-2 text-danger">
|
<div className="flex items-start gap-2 text-danger">
|
||||||
<svg className="h-5 w-5 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="h-5 w-5 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -439,9 +387,6 @@ export function UploadMenuDialog({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SidebarDialog>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalFrame>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,4 @@ export * from './switch';
|
||||||
export * from './tokens';
|
export * from './tokens';
|
||||||
export * from './toolbar';
|
export * from './toolbar';
|
||||||
export * from './variants';
|
export * from './variants';
|
||||||
|
export * from './sidebar-dialog';
|
||||||
|
|
|
||||||
128
src/components/ui/sidebar-dialog.tsx
Normal file
128
src/components/ui/sidebar-dialog.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { ModalFrame, ModalTitle, type DialogSize } from './dialog';
|
||||||
|
import { SidebarNav, SidebarNavItem } from './sidebar-nav';
|
||||||
|
import { cn } from './cn';
|
||||||
|
|
||||||
|
export type SidebarDialogSection<T extends string = string> = {
|
||||||
|
id: T;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SidebarDialog<T extends string = string>({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
size = 'xl',
|
||||||
|
panelTestId,
|
||||||
|
modalClassName,
|
||||||
|
headerTitle,
|
||||||
|
headerRight,
|
||||||
|
showCloseButton = true,
|
||||||
|
sections,
|
||||||
|
activeSectionId,
|
||||||
|
onSectionChange,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
customContent,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
size?: DialogSize;
|
||||||
|
panelTestId?: string;
|
||||||
|
modalClassName?: string;
|
||||||
|
headerTitle: ReactNode;
|
||||||
|
headerRight?: ReactNode;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
sections: SidebarDialogSection<T>[];
|
||||||
|
activeSectionId: T;
|
||||||
|
onSectionChange: (id: T) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
customContent?: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ModalFrame
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
size={size}
|
||||||
|
panelTestId={panelTestId}
|
||||||
|
className={modalClassName}
|
||||||
|
>
|
||||||
|
{customContent ? (
|
||||||
|
customContent
|
||||||
|
) : (
|
||||||
|
<div className={cn('flex flex-col h-[480px]', className)}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-3 border-b border-line-soft shrink-0">
|
||||||
|
<div className="flex items-baseline gap-4">
|
||||||
|
<ModalTitle>{headerTitle}</ModalTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{headerRight}
|
||||||
|
{showCloseButton && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-md p-1 text-soft hover:bg-accent-wash hover:text-foreground transition-colors duration-base"
|
||||||
|
aria-label="Close dialog"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation (Horizontal row of items) */}
|
||||||
|
<SidebarNav layout="grid" className="sm:hidden border-b border-line-soft bg-background p-2 shrink-0">
|
||||||
|
{sections.map((section) => {
|
||||||
|
const Icon = section.icon;
|
||||||
|
return (
|
||||||
|
<SidebarNavItem
|
||||||
|
compact
|
||||||
|
key={section.id}
|
||||||
|
onClick={() => onSectionChange(section.id)}
|
||||||
|
active={activeSectionId === section.id}
|
||||||
|
icon={<Icon className="w-3.5 h-3.5" />}
|
||||||
|
label={section.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarNav>
|
||||||
|
|
||||||
|
{/* Main Body (Split layout on desktop) */}
|
||||||
|
<div className="flex flex-row min-h-0 flex-1">
|
||||||
|
{/* Desktop Navigation (Left sidebar) */}
|
||||||
|
<nav className="hidden sm:block w-fit shrink-0 border-r border-line-soft bg-background p-2">
|
||||||
|
<SidebarNav>
|
||||||
|
{sections.map((section) => {
|
||||||
|
const Icon = section.icon;
|
||||||
|
const active = activeSectionId === section.id;
|
||||||
|
return (
|
||||||
|
<SidebarNavItem
|
||||||
|
key={section.id}
|
||||||
|
onClick={() => onSectionChange(section.id)}
|
||||||
|
active={active}
|
||||||
|
icon={<Icon className="w-4 h-4" />}
|
||||||
|
label={section.label}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarNav>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Tab Panel Content (Right detail view) */}
|
||||||
|
<div className={cn('flex-1 min-w-0 p-4 overflow-y-auto bg-surface', contentClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ function isPrivateIp(ip: string): boolean {
|
||||||
|
|
||||||
const ipv4Match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
const ipv4Match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||||
if (ipv4Match) {
|
if (ipv4Match) {
|
||||||
const [, o1, o2, o3, o4] = ipv4Match.map(Number);
|
const [, o1, o2] = ipv4Match.map(Number);
|
||||||
if (o1 === 10) return true; // 10.0.0.0/8
|
if (o1 === 10) return true; // 10.0.0.0/8
|
||||||
if (o1 === 172 && o2 >= 16 && o2 <= 31) return true; // 172.16.0.0/12
|
if (o1 === 172 && o2 >= 16 && o2 <= 31) return true; // 172.16.0.0/12
|
||||||
if (o1 === 192 && o2 === 168) return true; // 192.168.0.0/16
|
if (o1 === 192 && o2 === 168) return true; // 192.168.0.0/16
|
||||||
|
|
@ -211,7 +211,7 @@ export async function fetchAndParseUrl(
|
||||||
const turndownService = new TurndownService({
|
const turndownService = new TurndownService({
|
||||||
headingStyle: 'atx',
|
headingStyle: 'atx',
|
||||||
hr: '---',
|
hr: '---',
|
||||||
bullet: '-',
|
bulletListMarker: '-',
|
||||||
codeBlockStyle: 'fenced',
|
codeBlockStyle: 'fenced',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue