+
+
+ Source Web URL
+
+
+ setWebUrl(e.target.value)}
+ placeholder="https://en.wikipedia.org/wiki/Speed_reading"
+ className="flex-1"
+ disabled={importStep !== 'idle' && importStep !== 'error'}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleImportUrl();
}
}}
/>
+
+ Import
+
- )}
+
+ Extracts the central article body, removes boilerplate noise (headers, sidebars, ads), and converts it into a clean Markdown document for synchrony reading.
+
+
- {/* TAB 2: Create Custom Document */}
- {activeTab === 'create' && (
-
-
-
-
- setDocName(e.target.value)}
- placeholder="Document title (e.g. notes)"
- className="w-full font-medium"
- controlSize="md"
- disabled={isCreatingDoc}
- />
-
-
-
- ariaLabel="Document format selection"
- value={docExtension}
- options={[
- { value: '.md', label: 'Markdown (.md)' },
- { value: '.txt', label: 'Plain Text (.txt)' },
- ]}
- onChange={setDocExtension}
- className="grid-cols-2"
- />
-
-
-
-
-
-
-
-
-
- {docContent.length} character{docContent.length === 1 ? '' : 's'}
-
-
- {
- setDocName('');
- setDocContent('');
- }}
- disabled={isCreatingDoc}
- >
- Clear
-
-
- {isCreatingDoc ? 'Creating...' : 'Create'}
-
-
-
-
- )}
-
- {/* TAB 3: Import Web URL */}
- {activeTab === 'url' && (
-
-
-
-
- Source Web URL
-
-
-
setWebUrl(e.target.value)}
- placeholder="https://en.wikipedia.org/wiki/Speed_reading"
- className="flex-1"
- disabled={importStep !== 'idle' && importStep !== 'error'}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- handleImportUrl();
- }
- }}
- />
-
+ {importStep === 'error' ? (
+
+
+
+
+
+
Import Failed
+
+ {importError || 'An unknown error occurred.'}
+
+
setImportStep('idle')}
+ className="text-[11px] font-medium text-accent hover:underline mt-2 flex items-center gap-1"
>
- Import
-
+
Try Again
+
-
- Extracts the central article body, removes boilerplate noise (headers, sidebars, ads), and converts it into a clean Markdown document for synchrony reading.
-
-
- {/* Progress / Loading Stepper */}
- {importStep !== 'idle' && (
-
- {importStep === 'error' ? (
-
-
-
-
-
-
Import Failed
-
- {importError || 'An unknown error occurred.'}
-
-
setImportStep('idle')}
- className="text-[11px] font-medium text-accent hover:underline mt-2 flex items-center gap-1"
- >
- Try Again
-
-
-
- ) : (
-
-
-
-
-
Importing Article
-
Please hold on, converting document...
-
-
-
- {/* Visual Progress Steps */}
-
-
-
- {importStep === 'converting' || importStep === 'uploading' ? (
-
- ) : '1'}
-
- Scraping Page
-
-
-
- {importStep === 'uploading' ? (
-
- ) : '2'}
-
- Extracting Text
-
-
-
- 3
-
- Uploading
-
-
-
- )}
+ ) : (
+
+
+
+
+
Importing Article
+
Please hold on, converting document...
+
- )}
-
+
+ {/* Visual Progress Steps */}
+
+
+
+ {importStep === 'converting' || importStep === 'uploading' ? (
+
+ ) : '1'}
+
+ Scraping Page
+
+
+
+ {importStep === 'uploading' ? (
+
+ ) : '2'}
+
+ Extracting Text
+
+
+
+ 3
+
+ Uploading
+
+
+
+ )}
)}
-
-
+ )}
+
);
}
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index 0e9dda5..91d5641 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -23,3 +23,4 @@ export * from './switch';
export * from './tokens';
export * from './toolbar';
export * from './variants';
+export * from './sidebar-dialog';
diff --git a/src/components/ui/sidebar-dialog.tsx b/src/components/ui/sidebar-dialog.tsx
new file mode 100644
index 0000000..cb4f88c
--- /dev/null
+++ b/src/components/ui/sidebar-dialog.tsx
@@ -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
= {
+ id: T;
+ label: string;
+ icon: React.ComponentType>;
+};
+
+export function SidebarDialog({
+ 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[];
+ activeSectionId: T;
+ onSectionChange: (id: T) => void;
+ children: ReactNode;
+ className?: string;
+ contentClassName?: string;
+ customContent?: ReactNode;
+}) {
+ return (
+
+ {customContent ? (
+ customContent
+ ) : (
+
+ {/* Header */}
+
+
+ {headerTitle}
+
+
+ {headerRight}
+ {showCloseButton && (
+
+
+
+
+
+ )}
+
+
+
+ {/* Mobile Navigation (Horizontal row of items) */}
+
+ {sections.map((section) => {
+ const Icon = section.icon;
+ return (
+ onSectionChange(section.id)}
+ active={activeSectionId === section.id}
+ icon={ }
+ label={section.label}
+ />
+ );
+ })}
+
+
+ {/* Main Body (Split layout on desktop) */}
+
+ {/* Desktop Navigation (Left sidebar) */}
+
+
+ {sections.map((section) => {
+ const Icon = section.icon;
+ const active = activeSectionId === section.id;
+ return (
+ onSectionChange(section.id)}
+ active={active}
+ icon={ }
+ label={section.label}
+ className="whitespace-nowrap"
+ />
+ );
+ })}
+
+
+
+ {/* Tab Panel Content (Right detail view) */}
+
+ {children}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/lib/server/documents/web-loader.ts b/src/lib/server/documents/web-loader.ts
index 9a68357..e247c25 100644
--- a/src/lib/server/documents/web-loader.ts
+++ b/src/lib/server/documents/web-loader.ts
@@ -15,7 +15,7 @@ function isPrivateIp(ip: string): boolean {
const ipv4Match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
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 === 172 && o2 >= 16 && o2 <= 31) return true; // 172.16.0.0/12
if (o1 === 192 && o2 === 168) return true; // 192.168.0.0/16
@@ -211,7 +211,7 @@ export async function fetchAndParseUrl(
const turndownService = new TurndownService({
headingStyle: 'atx',
hr: '---',
- bullet: '-',
+ bulletListMarker: '-',
codeBlockStyle: 'fenced',
});