@geminilight/mindos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local.example +38 -0
- package/LICENSE +21 -0
- package/README.md +423 -0
- package/README_zh.md +423 -0
- package/app/README.md +152 -0
- package/app/app/api/ask/route.ts +170 -0
- package/app/app/api/ask-sessions/route.ts +90 -0
- package/app/app/api/auth/route.ts +37 -0
- package/app/app/api/backlinks/route.ts +22 -0
- package/app/app/api/bootstrap/route.ts +37 -0
- package/app/app/api/extract-pdf/route.ts +82 -0
- package/app/app/api/file/route.ts +138 -0
- package/app/app/api/files/route.ts +12 -0
- package/app/app/api/git/route.ts +42 -0
- package/app/app/api/graph/route.ts +113 -0
- package/app/app/api/recent-files/route.ts +10 -0
- package/app/app/api/search/route.ts +17 -0
- package/app/app/api/settings/reset-token/route.ts +21 -0
- package/app/app/api/settings/route.ts +123 -0
- package/app/app/error.tsx +33 -0
- package/app/app/globals.css +368 -0
- package/app/app/icon.svg +35 -0
- package/app/app/layout.tsx +103 -0
- package/app/app/login/page.tsx +120 -0
- package/app/app/page.tsx +12 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +343 -0
- package/app/app/view/[...path]/error.tsx +33 -0
- package/app/app/view/[...path]/loading.tsx +15 -0
- package/app/app/view/[...path]/page.tsx +93 -0
- package/app/components/AskFab.tsx +59 -0
- package/app/components/AskModal.tsx +398 -0
- package/app/components/Backlinks.tsx +75 -0
- package/app/components/Breadcrumb.tsx +31 -0
- package/app/components/CsvView.tsx +325 -0
- package/app/components/DirView.tsx +138 -0
- package/app/components/Editor.tsx +124 -0
- package/app/components/EditorWrapper.tsx +17 -0
- package/app/components/ErrorBoundary.tsx +53 -0
- package/app/components/FileTree.tsx +369 -0
- package/app/components/HomeContent.tsx +262 -0
- package/app/components/JsonView.tsx +27 -0
- package/app/components/MarkdownEditor.tsx +95 -0
- package/app/components/MarkdownView.tsx +118 -0
- package/app/components/SearchModal.tsx +193 -0
- package/app/components/SettingsModal.tsx +237 -0
- package/app/components/Sidebar.tsx +136 -0
- package/app/components/SidebarLayout.tsx +36 -0
- package/app/components/TableOfContents.tsx +150 -0
- package/app/components/ThemeToggle.tsx +34 -0
- package/app/components/WysiwygEditor.tsx +75 -0
- package/app/components/ask/FileChip.tsx +30 -0
- package/app/components/ask/MentionPopover.tsx +52 -0
- package/app/components/ask/MessageList.tsx +126 -0
- package/app/components/ask/SessionHistory.tsx +49 -0
- package/app/components/renderers/AgentInspectorRenderer.tsx +277 -0
- package/app/components/renderers/BacklinksRenderer.tsx +147 -0
- package/app/components/renderers/ConfigRenderer.tsx +236 -0
- package/app/components/renderers/CsvRenderer.tsx +77 -0
- package/app/components/renderers/DiffRenderer.tsx +310 -0
- package/app/components/renderers/GraphRenderer.tsx +428 -0
- package/app/components/renderers/SummaryRenderer.tsx +251 -0
- package/app/components/renderers/TimelineRenderer.tsx +213 -0
- package/app/components/renderers/TodoRenderer.tsx +474 -0
- package/app/components/renderers/WorkflowRenderer.tsx +404 -0
- package/app/components/renderers/csv/BoardView.tsx +146 -0
- package/app/components/renderers/csv/ConfigPanel.tsx +117 -0
- package/app/components/renderers/csv/EditableCell.tsx +43 -0
- package/app/components/renderers/csv/GalleryView.tsx +40 -0
- package/app/components/renderers/csv/TableView.tsx +164 -0
- package/app/components/renderers/csv/types.ts +87 -0
- package/app/components/settings/AiTab.tsx +111 -0
- package/app/components/settings/AppearanceTab.tsx +101 -0
- package/app/components/settings/KnowledgeTab.tsx +157 -0
- package/app/components/settings/PluginsTab.tsx +82 -0
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/ShortcutsTab.tsx +22 -0
- package/app/components/settings/types.ts +41 -0
- package/app/components/ui/button.tsx +60 -0
- package/app/components/ui/dialog.tsx +157 -0
- package/app/components/ui/input.tsx +20 -0
- package/app/components/ui/scroll-area.tsx +55 -0
- package/app/components/ui/toggle.tsx +44 -0
- package/app/components/ui/tooltip.tsx +66 -0
- package/app/components.json +25 -0
- package/app/data/pages/home-dark.png +0 -0
- package/app/data/pages/home-mobile-crop.png +0 -0
- package/app/data/pages/home-mobile.png +0 -0
- package/app/data/pages/home.png +0 -0
- package/app/data/pages/view-dir.png +0 -0
- package/app/data/pages/view-file-bot.png +0 -0
- package/app/data/pages/view-file-dark-crop.png +0 -0
- package/app/data/pages/view-file-dark.png +0 -0
- package/app/data/pages/view-file-mobile.png +0 -0
- package/app/data/pages/view-file-sm.png +0 -0
- package/app/data/pages/view-file-top.png +0 -0
- package/app/data/pages/view-file.png +0 -0
- package/app/eslint.config.mjs +18 -0
- package/app/hooks/useAskSession.ts +181 -0
- package/app/hooks/useFileUpload.ts +126 -0
- package/app/hooks/useMention.ts +65 -0
- package/app/lib/LocaleContext.tsx +40 -0
- package/app/lib/actions.ts +40 -0
- package/app/lib/agent/index.ts +3 -0
- package/app/lib/agent/model.ts +18 -0
- package/app/lib/agent/prompt.ts +32 -0
- package/app/lib/agent/tools.ts +151 -0
- package/app/lib/api.ts +55 -0
- package/app/lib/core/backlinks.ts +40 -0
- package/app/lib/core/csv.ts +28 -0
- package/app/lib/core/fs-ops.ts +118 -0
- package/app/lib/core/git.ts +50 -0
- package/app/lib/core/index.ts +58 -0
- package/app/lib/core/lines.ts +89 -0
- package/app/lib/core/search.ts +79 -0
- package/app/lib/core/security.ts +43 -0
- package/app/lib/core/tree.ts +113 -0
- package/app/lib/core/types.ts +40 -0
- package/app/lib/fs.ts +467 -0
- package/app/lib/i18n.ts +300 -0
- package/app/lib/jwt.ts +58 -0
- package/app/lib/renderers/index.ts +79 -0
- package/app/lib/renderers/registry.ts +70 -0
- package/app/lib/settings.ts +150 -0
- package/app/lib/types.ts +32 -0
- package/app/lib/utils.ts +34 -0
- package/app/next-env.d.ts +6 -0
- package/app/next.config.ts +10 -0
- package/app/package-lock.json +15306 -0
- package/app/package.json +71 -0
- package/app/postcss.config.mjs +7 -0
- package/app/proxy.ts +64 -0
- package/app/public/file.svg +1 -0
- package/app/public/globe.svg +1 -0
- package/app/public/landing/index.html +353 -0
- package/app/public/landing/style.css +216 -0
- package/app/public/logo-square.svg +37 -0
- package/app/public/logo.svg +37 -0
- package/app/public/next.svg +1 -0
- package/app/public/vercel.svg +1 -0
- package/app/public/window.svg +1 -0
- package/app/scripts/extract-pdf.cjs +56 -0
- package/app/tsconfig.json +34 -0
- package/app/vitest.config.ts +14 -0
- package/assets/demo-flow-zh.html +622 -0
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/assets/images/gui-sync-cv.png +0 -0
- package/assets/logo-square.svg +37 -0
- package/bin/cli.js +894 -0
- package/mcp/README.md +113 -0
- package/mcp/package-lock.json +1717 -0
- package/mcp/package.json +18 -0
- package/mcp/src/index.ts +494 -0
- package/mcp/tsconfig.json +13 -0
- package/package.json +49 -0
- package/scripts/setup.js +675 -0
- package/scripts/upgrade-prompt.md +147 -0
- package/skills/mindos/SKILL.md +319 -0
- package/skills/mindos-zh/SKILL.md +318 -0
- package/templates/README.md +31 -0
- package/templates/empty/CHANGELOG.md +9 -0
- package/templates/empty/CONFIG.json +197 -0
- package/templates/empty/CONFIG.md +73 -0
- package/templates/empty/INSTRUCTION.md +177 -0
- package/templates/empty/README.md +27 -0
- package/templates/en/CHANGELOG.md +9 -0
- package/templates/en/CONFIG.json +197 -0
- package/templates/en/CONFIG.md +73 -0
- package/templates/en/INSTRUCTION.md +177 -0
- package/templates/en/README.md +27 -0
- package/templates/en/TODO.md +13 -0
- package/templates/en//360/237/221/244 Profile/INSTRUCTION.md" +21 -0
- package/templates/en//360/237/221/244 Profile/README.md" +15 -0
- package/templates/en//360/237/221/244 Profile//342/232/231/357/270/217 Preferences.md" +21 -0
- package/templates/en//360/237/221/244 Profile//360/237/216/257 Focus.md" +31 -0
- package/templates/en//360/237/221/244 Profile//360/237/221/244 Identity.md" +22 -0
- package/templates/en//360/237/223/232 Resources/INSTRUCTION.md" +29 -0
- package/templates/en//360/237/223/232 Resources/README.md" +21 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +1 -0
- package/templates/en//360/237/223/235 Notes/INSTRUCTION.md" +31 -0
- package/templates/en//360/237/223/235 Notes/Ideas/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +16 -0
- package/templates/en//360/237/223/235 Notes/Inbox/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Inbox//360/237/247/252_example_quick_capture.md" +14 -0
- package/templates/en//360/237/223/235 Notes/Meetings/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Meetings//360/237/247/252_example_meeting_note.md" +17 -0
- package/templates/en//360/237/223/235 Notes/README.md" +24 -0
- package/templates/en//360/237/223/235 Notes/Waiting/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Waiting//360/237/247/252_example_blocked_item.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Configurations/README.md" +3 -0
- package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +14 -0
- package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +21 -0
- package/templates/en//360/237/224/204 Workflows/Information/README.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Information//360/237/247/252_example_info_capture_sop.md" +13 -0
- package/templates/en//360/237/224/204 Workflows/Media/README.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Media//360/237/247/252_example_content_publish_sop.md" +13 -0
- package/templates/en//360/237/224/204 Workflows/README.md" +22 -0
- package/templates/en//360/237/224/204 Workflows/Research/README.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Research//360/237/247/252_example_lit_review_sop.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Startup/README.md" +3 -0
- package/templates/en//360/237/224/204 Workflows/Startup//360/237/247/252_example_weekly_founder_ops.md" +22 -0
- package/templates/en//360/237/224/227 Connections/Classmates/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Classmates//360/237/247/252_example_leo_chen.md" +16 -0
- package/templates/en//360/237/224/227 Connections/Colleagues/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Colleagues//360/237/247/252_example_ethan_zhao.md" +16 -0
- package/templates/en//360/237/224/227 Connections/Connections Overview.csv" +5 -0
- package/templates/en//360/237/224/227 Connections/Family/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Family//360/237/247/252_example_james_wang.md" +16 -0
- package/templates/en//360/237/224/227 Connections/Friends/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Friends//360/237/247/252_example_lily_lin.md" +16 -0
- package/templates/en//360/237/224/227 Connections/INSTRUCTION.md" +56 -0
- package/templates/en//360/237/224/227 Connections/README.md" +20 -0
- package/templates/en//360/237/232/200 Projects/Archived/README.md" +9 -0
- package/templates/en//360/237/232/200 Projects/Archived//360/237/247/252_example_archived_project_note.md" +14 -0
- package/templates/en//360/237/232/200 Projects/INSTRUCTION.md" +29 -0
- package/templates/en//360/237/232/200 Projects/Products/README.md" +16 -0
- package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +20 -0
- package/templates/en//360/237/232/200 Projects/README.md" +21 -0
- package/templates/en//360/237/232/200 Projects/Research/README.md" +16 -0
- package/templates/en//360/237/232/200 Projects/Research//360/237/247/252_example_research_project_brief.md" +16 -0
- package/templates/template-generation-skill.md +79 -0
- package/templates/zh/CHANGELOG.md +9 -0
- package/templates/zh/CONFIG.json +197 -0
- package/templates/zh/CONFIG.md +66 -0
- package/templates/zh/INSTRUCTION.md +177 -0
- package/templates/zh/README.md +27 -0
- package/templates/zh/TODO.md +13 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217/INSTRUCTION.md" +28 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +20 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/217 Preferences.md" +21 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/257 Focus.md" +31 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/244 Identity.md" +22 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220/INSTRUCTION.md" +29 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +21 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +1 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /344/272/247/345/223/201.csv" +1 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/255/246/350/200/205/346/270/205/345/215/225.csv" +1 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/267/245/345/205/267/346/270/205/345/215/225.csv" +1 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260/INSTRUCTION.md" +31 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260/README.md" +24 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256/README.md" +8 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256//360/237/247/252_example_/344/274/232/350/256/256/347/272/252/350/246/201.md" +17 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//345/276/205/345/217/215/351/246/210/README.md" +8 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//345/276/205/345/217/215/351/246/210//360/237/247/252_example_/345/276/205/345/217/215/351/246/210/344/272/213/351/241/271.md" +16 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225/README.md" +8 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225//360/237/247/252_example_/344/272/247/345/223/201/346/203/263/346/263/225.md" +16 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/224/266/344/273/266/347/256/261/README.md" +8 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/224/266/344/273/266/347/256/261//360/237/247/252_example_/344/270/264/346/227/266/351/200/237/350/256/260.md" +13 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213/INSTRUCTION.md" +29 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +21 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257/README.md" +16 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257//360/237/247/252_example_/344/277/241/346/201/257/351/207/207/351/233/206/346/265/201/347/250/213.md" +13 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223/README.md" +16 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223//360/237/247/252_example_/345/206/205/345/256/271/345/217/221/345/270/203/346/265/201/347/250/213.md" +13 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224/README.md" +16 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224//360/237/247/252_example_/346/226/207/347/214/256/347/273/274/350/277/260/346/265/201/347/250/213.md" +16 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256/README.md" +3 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256//360/237/247/252_example_/351/205/215/347/275/256/346/233/264/346/226/260/346/265/201/347/250/213.md" +26 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273/INSTRUCTION.md" +62 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273/README.md" +20 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/205/263/347/263/273/346/200/273/350/247/210.csv" +5 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213/README.md" +11 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213//360/237/247/252_example_/345/220/214/344/272/213/350/265/265/344/270/200/350/276/260.md" +16 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246/README.md" +11 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246//360/237/247/252_example_/345/220/214/345/255/246/351/231/210/347/253/213/346/254/247.md" +16 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272/README.md" +11 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272//360/237/247/252_example_/345/256/266/344/272/272/347/216/213/345/273/272/345/233/275.md" +16 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213/README.md" +11 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213//360/237/247/252_example_/346/234/213/345/217/213/346/236/227/345/260/217/344/270/275.md" +16 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256/INSTRUCTION.md" +31 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256/README.md" +21 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201/README.md" +16 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201//360/237/247/252_example_/344/272/247/345/223/201/351/241/271/347/233/256/347/256/200/346/212/245.md" +20 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//345/267/262/345/275/222/346/241/243/README.md" +9 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//345/267/262/345/275/222/346/241/243//360/237/247/252_example_/345/275/222/346/241/243/351/241/271/347/233/256/350/256/260/345/275/225.md" +15 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224/README.md" +16 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224//360/237/247/252_example_/347/247/221/347/240/224/351/241/271/347/233/256/347/256/200/346/212/245.md" +16 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
import Papa from 'papaparse';
|
|
5
|
+
import { ChevronUp, ChevronDown, ChevronsUpDown, Plus, Trash2 } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
interface CsvViewProps {
|
|
8
|
+
content: string;
|
|
9
|
+
filePath?: string;
|
|
10
|
+
appendAction?: (newRow: string[]) => Promise<{ newContent: string }>;
|
|
11
|
+
saveAction?: (newContent: string) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type SortDir = 'asc' | 'desc' | null;
|
|
15
|
+
|
|
16
|
+
function serializeRows(headers: string[], rows: string[][]): string {
|
|
17
|
+
return Papa.unparse([headers, ...rows]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Inline cell editor ───────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function Cell({
|
|
23
|
+
value,
|
|
24
|
+
editable,
|
|
25
|
+
onCommit,
|
|
26
|
+
}: {
|
|
27
|
+
value: string;
|
|
28
|
+
editable: boolean;
|
|
29
|
+
onCommit: (newVal: string) => void;
|
|
30
|
+
}) {
|
|
31
|
+
const [editing, setEditing] = useState(false);
|
|
32
|
+
const [draft, setDraft] = useState(value);
|
|
33
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
34
|
+
|
|
35
|
+
function startEdit() {
|
|
36
|
+
if (!editable) return;
|
|
37
|
+
setDraft(value);
|
|
38
|
+
setEditing(true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (editing) inputRef.current?.select();
|
|
43
|
+
}, [editing]);
|
|
44
|
+
|
|
45
|
+
function commit() {
|
|
46
|
+
setEditing(false);
|
|
47
|
+
if (draft !== value) onCommit(draft);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (editing) {
|
|
51
|
+
return (
|
|
52
|
+
<input
|
|
53
|
+
ref={inputRef}
|
|
54
|
+
value={draft}
|
|
55
|
+
onChange={e => setDraft(e.target.value)}
|
|
56
|
+
onBlur={commit}
|
|
57
|
+
onKeyDown={e => {
|
|
58
|
+
if (e.key === 'Enter') commit();
|
|
59
|
+
if (e.key === 'Escape') { setDraft(value); setEditing(false); }
|
|
60
|
+
}}
|
|
61
|
+
className="w-full bg-transparent outline-none text-sm"
|
|
62
|
+
style={{
|
|
63
|
+
color: 'var(--foreground)',
|
|
64
|
+
borderBottom: '1px solid var(--amber)',
|
|
65
|
+
minWidth: 60,
|
|
66
|
+
}}
|
|
67
|
+
onClick={e => e.stopPropagation()}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
className={`truncate text-sm ${editable ? 'cursor-text' : ''}`}
|
|
75
|
+
style={{ color: 'var(--foreground)', minWidth: 60 }}
|
|
76
|
+
onClick={editable ? startEdit : undefined}
|
|
77
|
+
title={value}
|
|
78
|
+
>
|
|
79
|
+
{value || <span style={{ color: 'var(--muted-foreground)', opacity: 0.4 }}>—</span>}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Add row form ─────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function AddRowForm({
|
|
87
|
+
headers,
|
|
88
|
+
onAdd,
|
|
89
|
+
onCancel,
|
|
90
|
+
}: {
|
|
91
|
+
headers: string[];
|
|
92
|
+
onAdd: (row: string[]) => void;
|
|
93
|
+
onCancel: () => void;
|
|
94
|
+
}) {
|
|
95
|
+
const [values, setValues] = useState<string[]>(() => Array(headers.length).fill(''));
|
|
96
|
+
const firstRef = useRef<HTMLInputElement>(null);
|
|
97
|
+
|
|
98
|
+
function set(i: number, v: string) {
|
|
99
|
+
setValues(prev => { const next = [...prev]; next[i] = v; return next; });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function submit() {
|
|
103
|
+
onAdd(values);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<tr style={{ background: 'color-mix(in srgb, var(--amber) 6%, transparent)', borderTop: '1px solid var(--amber)' }}>
|
|
108
|
+
{headers.map((h, i) => (
|
|
109
|
+
<td key={i} className="px-3 py-2" style={{ borderBottom: '1px solid var(--border)' }}>
|
|
110
|
+
<input
|
|
111
|
+
ref={i === 0 ? firstRef : undefined}
|
|
112
|
+
autoFocus={i === 0}
|
|
113
|
+
value={values[i]}
|
|
114
|
+
onChange={e => set(i, e.target.value)}
|
|
115
|
+
onKeyDown={e => {
|
|
116
|
+
if (e.key === 'Enter') submit();
|
|
117
|
+
if (e.key === 'Escape') onCancel();
|
|
118
|
+
}}
|
|
119
|
+
placeholder={h}
|
|
120
|
+
className="w-full bg-transparent outline-none text-sm placeholder:opacity-30"
|
|
121
|
+
style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--border)' }}
|
|
122
|
+
/>
|
|
123
|
+
</td>
|
|
124
|
+
))}
|
|
125
|
+
{/* spacer for delete column */}
|
|
126
|
+
<td className="px-2 py-2" style={{ borderBottom: '1px solid var(--border)' }} />
|
|
127
|
+
</tr>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Main component ───────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
export default function CsvView({ content: initialContent, appendAction, saveAction }: CsvViewProps) {
|
|
134
|
+
const [content, setContent] = useState(initialContent);
|
|
135
|
+
const [sortCol, setSortCol] = useState<number | null>(null);
|
|
136
|
+
const [sortDir, setSortDir] = useState<SortDir>(null);
|
|
137
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
138
|
+
|
|
139
|
+
const parsed = useMemo(() => {
|
|
140
|
+
const result = Papa.parse<string[]>(content, { skipEmptyLines: true });
|
|
141
|
+
return result.data as string[][];
|
|
142
|
+
}, [content]);
|
|
143
|
+
|
|
144
|
+
const headers = parsed[0] || [];
|
|
145
|
+
const rows = parsed.slice(1);
|
|
146
|
+
|
|
147
|
+
const sortedRows = useMemo(() => {
|
|
148
|
+
if (sortCol === null || sortDir === null) return rows;
|
|
149
|
+
return [...rows].sort((a, b) => {
|
|
150
|
+
const va = a[sortCol] ?? '', vb = b[sortCol] ?? '';
|
|
151
|
+
const na = parseFloat(va), nb = parseFloat(vb);
|
|
152
|
+
if (!isNaN(na) && !isNaN(nb)) return sortDir === 'asc' ? na - nb : nb - na;
|
|
153
|
+
return sortDir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
154
|
+
});
|
|
155
|
+
}, [rows, sortCol, sortDir]);
|
|
156
|
+
|
|
157
|
+
function toggleSort(col: number) {
|
|
158
|
+
if (sortCol !== col) { setSortCol(col); setSortDir('asc'); return; }
|
|
159
|
+
if (sortDir === 'asc') { setSortDir('desc'); return; }
|
|
160
|
+
setSortCol(null); setSortDir(null);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update a single cell and persist
|
|
164
|
+
const handleCellCommit = useCallback(async (rowIdx: number, colIdx: number, newVal: string) => {
|
|
165
|
+
if (!saveAction) return;
|
|
166
|
+
// rowIdx here is index into sortedRows — need to map back to original rows
|
|
167
|
+
const updatedRows = rows.map((r, i) => {
|
|
168
|
+
// find which original row matches this sorted row
|
|
169
|
+
const sorted = sortedRows[rowIdx];
|
|
170
|
+
if (r === sorted) return r.map((cell, ci) => ci === colIdx ? newVal : cell);
|
|
171
|
+
return r;
|
|
172
|
+
});
|
|
173
|
+
const newContent = serializeRows(headers, updatedRows);
|
|
174
|
+
setContent(newContent);
|
|
175
|
+
await saveAction(newContent);
|
|
176
|
+
}, [saveAction, rows, sortedRows, headers]);
|
|
177
|
+
|
|
178
|
+
// Delete a row and persist
|
|
179
|
+
const handleDeleteRow = useCallback(async (rowIdx: number) => {
|
|
180
|
+
if (!saveAction) return;
|
|
181
|
+
const sorted = sortedRows[rowIdx];
|
|
182
|
+
const updatedRows = rows.filter(r => r !== sorted);
|
|
183
|
+
const newContent = serializeRows(headers, updatedRows);
|
|
184
|
+
setContent(newContent);
|
|
185
|
+
await saveAction(newContent);
|
|
186
|
+
}, [saveAction, rows, sortedRows, headers]);
|
|
187
|
+
|
|
188
|
+
// Append a new row
|
|
189
|
+
const handleAddRow = useCallback(async (newRow: string[]) => {
|
|
190
|
+
setShowAdd(false);
|
|
191
|
+
if (appendAction) {
|
|
192
|
+
const result = await appendAction(newRow);
|
|
193
|
+
setContent(result.newContent);
|
|
194
|
+
} else if (saveAction) {
|
|
195
|
+
const newContent = serializeRows(headers, [...rows, newRow]);
|
|
196
|
+
setContent(newContent);
|
|
197
|
+
await saveAction(newContent);
|
|
198
|
+
}
|
|
199
|
+
}, [appendAction, saveAction, headers, rows]);
|
|
200
|
+
|
|
201
|
+
const canEdit = !!saveAction || !!appendAction;
|
|
202
|
+
|
|
203
|
+
if (headers.length === 0) {
|
|
204
|
+
return <div className="text-sm py-4" style={{ color: 'var(--muted-foreground)' }}>Empty CSV file</div>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="w-full rounded-xl overflow-hidden border" style={{ borderColor: 'var(--border)' }}>
|
|
209
|
+
<div className="overflow-x-auto">
|
|
210
|
+
<table className="min-w-full text-sm border-collapse">
|
|
211
|
+
<thead>
|
|
212
|
+
<tr style={{ background: 'var(--muted)' }}>
|
|
213
|
+
{headers.map((header, i) => (
|
|
214
|
+
<th
|
|
215
|
+
key={i}
|
|
216
|
+
onClick={() => toggleSort(i)}
|
|
217
|
+
className="px-4 py-2.5 text-left font-semibold cursor-pointer select-none whitespace-nowrap transition-colors hover:bg-accent"
|
|
218
|
+
style={{
|
|
219
|
+
color: 'var(--foreground)',
|
|
220
|
+
borderBottom: '1px solid var(--border)',
|
|
221
|
+
fontFamily: "'IBM Plex Sans', sans-serif",
|
|
222
|
+
fontSize: '0.75rem',
|
|
223
|
+
letterSpacing: '0.04em',
|
|
224
|
+
textTransform: 'uppercase',
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<div className="flex items-center gap-1.5">
|
|
228
|
+
<span>{header}</span>
|
|
229
|
+
{sortCol === i
|
|
230
|
+
? sortDir === 'asc'
|
|
231
|
+
? <ChevronUp size={11} style={{ color: 'var(--amber)' }} />
|
|
232
|
+
: <ChevronDown size={11} style={{ color: 'var(--amber)' }} />
|
|
233
|
+
: <ChevronsUpDown size={11} style={{ color: 'var(--muted-foreground)', opacity: 0.4 }} />
|
|
234
|
+
}
|
|
235
|
+
</div>
|
|
236
|
+
</th>
|
|
237
|
+
))}
|
|
238
|
+
{canEdit && <th className="w-8" style={{ borderBottom: '1px solid var(--border)', background: 'var(--muted)' }} />}
|
|
239
|
+
</tr>
|
|
240
|
+
</thead>
|
|
241
|
+
<tbody>
|
|
242
|
+
{sortedRows.map((row, rowIdx) => (
|
|
243
|
+
<tr
|
|
244
|
+
key={rowIdx}
|
|
245
|
+
className="group transition-colors"
|
|
246
|
+
style={{
|
|
247
|
+
background: rowIdx % 2 === 0 ? 'var(--background)' : 'var(--card)',
|
|
248
|
+
}}
|
|
249
|
+
onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')}
|
|
250
|
+
onMouseLeave={e => (e.currentTarget.style.background = rowIdx % 2 === 0 ? 'var(--background)' : 'var(--card)')}
|
|
251
|
+
>
|
|
252
|
+
{headers.map((_, colIdx) => (
|
|
253
|
+
<td
|
|
254
|
+
key={colIdx}
|
|
255
|
+
className="px-4 py-2.5 max-w-xs"
|
|
256
|
+
style={{ borderBottom: '1px solid var(--border)' }}
|
|
257
|
+
>
|
|
258
|
+
<Cell
|
|
259
|
+
value={row[colIdx] ?? ''}
|
|
260
|
+
editable={!!saveAction}
|
|
261
|
+
onCommit={(v) => handleCellCommit(rowIdx, colIdx, v)}
|
|
262
|
+
/>
|
|
263
|
+
</td>
|
|
264
|
+
))}
|
|
265
|
+
{canEdit && (
|
|
266
|
+
<td className="px-2 py-2" style={{ borderBottom: '1px solid var(--border)' }}>
|
|
267
|
+
{saveAction && (
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => handleDeleteRow(rowIdx)}
|
|
270
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-destructive/10"
|
|
271
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
272
|
+
title="Delete row"
|
|
273
|
+
>
|
|
274
|
+
<Trash2 size={12} />
|
|
275
|
+
</button>
|
|
276
|
+
)}
|
|
277
|
+
</td>
|
|
278
|
+
)}
|
|
279
|
+
</tr>
|
|
280
|
+
))}
|
|
281
|
+
|
|
282
|
+
{/* Add row form */}
|
|
283
|
+
{showAdd && (
|
|
284
|
+
<AddRowForm
|
|
285
|
+
headers={headers}
|
|
286
|
+
onAdd={handleAddRow}
|
|
287
|
+
onCancel={() => setShowAdd(false)}
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
290
|
+
</tbody>
|
|
291
|
+
</table>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{/* Footer */}
|
|
295
|
+
<div
|
|
296
|
+
className="px-4 py-2 flex items-center justify-between"
|
|
297
|
+
style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}
|
|
298
|
+
>
|
|
299
|
+
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
300
|
+
{rows.length} rows · {headers.length} cols
|
|
301
|
+
</span>
|
|
302
|
+
|
|
303
|
+
{canEdit && !showAdd && (
|
|
304
|
+
<button
|
|
305
|
+
onClick={() => setShowAdd(true)}
|
|
306
|
+
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md transition-colors"
|
|
307
|
+
style={{ color: 'var(--amber)', background: 'var(--amber-dim)', fontFamily: "'IBM Plex Mono', monospace" }}
|
|
308
|
+
>
|
|
309
|
+
<Plus size={12} />
|
|
310
|
+
Add row
|
|
311
|
+
</button>
|
|
312
|
+
)}
|
|
313
|
+
{showAdd && (
|
|
314
|
+
<button
|
|
315
|
+
onClick={() => setShowAdd(false)}
|
|
316
|
+
className="text-xs px-2.5 py-1 rounded-md transition-colors"
|
|
317
|
+
style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
|
|
318
|
+
>
|
|
319
|
+
Cancel
|
|
320
|
+
</button>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
|
|
6
|
+
import Breadcrumb from '@/components/Breadcrumb';
|
|
7
|
+
import { encodePath } from '@/lib/utils';
|
|
8
|
+
import { FileNode } from '@/lib/types';
|
|
9
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
10
|
+
|
|
11
|
+
interface DirViewProps {
|
|
12
|
+
dirPath: string;
|
|
13
|
+
entries: FileNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function FileIcon({ node }: { node: FileNode }) {
|
|
17
|
+
if (node.type === 'directory') return <Folder size={16} className="text-yellow-400 shrink-0" />;
|
|
18
|
+
if (node.extension === '.csv') return <Table size={16} className="text-emerald-400 shrink-0" />;
|
|
19
|
+
return <FileText size={16} className="text-zinc-400 shrink-0" />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function FileIconLarge({ node }: { node: FileNode }) {
|
|
23
|
+
if (node.type === 'directory') return <FolderOpen size={28} className="text-yellow-400" />;
|
|
24
|
+
if (node.extension === '.csv') return <Table size={28} className="text-emerald-400" />;
|
|
25
|
+
return <FileText size={28} className="text-zinc-400" />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function countFiles(node: FileNode): number {
|
|
29
|
+
if (node.type === 'file') return 1;
|
|
30
|
+
return (node.children || []).reduce((acc, c) => acc + countFiles(c), 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DIR_VIEW_KEY = 'mindos-dir-view';
|
|
34
|
+
|
|
35
|
+
function useDirViewPref() {
|
|
36
|
+
const [view, setViewState] = useState<'grid' | 'list'>('grid');
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const saved = localStorage.getItem(DIR_VIEW_KEY);
|
|
40
|
+
if (saved === 'list' || saved === 'grid') setViewState(saved);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const setView = (v: 'grid' | 'list') => {
|
|
44
|
+
setViewState(v);
|
|
45
|
+
localStorage.setItem(DIR_VIEW_KEY, v);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return [view, setView] as const;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
52
|
+
const [view, setView] = useDirViewPref();
|
|
53
|
+
const { t } = useLocale();
|
|
54
|
+
const fileCounts = useMemo(() => {
|
|
55
|
+
const map = new Map<string, number>();
|
|
56
|
+
for (const e of entries) map.set(e.path, countFiles(e));
|
|
57
|
+
return map;
|
|
58
|
+
}, [entries]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col min-h-screen">
|
|
62
|
+
{/* Topbar */}
|
|
63
|
+
<div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5" style={{ background: 'var(--background)' }}>
|
|
64
|
+
<div className="max-w-[860px] mx-auto flex items-center justify-between gap-2">
|
|
65
|
+
<div className="min-w-0 flex-1">
|
|
66
|
+
<Breadcrumb filePath={dirPath} />
|
|
67
|
+
</div>
|
|
68
|
+
{/* View toggle */}
|
|
69
|
+
<div className="flex items-center gap-1 p-1 bg-muted rounded-lg shrink-0">
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => setView('grid')}
|
|
72
|
+
className={`p-1.5 rounded transition-colors ${view === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
73
|
+
title={t.dirView.gridView}
|
|
74
|
+
>
|
|
75
|
+
<LayoutGrid size={14} />
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => setView('list')}
|
|
79
|
+
className={`p-1.5 rounded transition-colors ${view === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
80
|
+
title={t.dirView.listView}
|
|
81
|
+
>
|
|
82
|
+
<List size={14} />
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Content */}
|
|
89
|
+
<div className="flex-1 px-4 md:px-6 py-6">
|
|
90
|
+
<div className="max-w-[860px] mx-auto">
|
|
91
|
+
{entries.length === 0 ? (
|
|
92
|
+
<p className="text-muted-foreground text-sm">{t.dirView.emptyFolder}</p>
|
|
93
|
+
) : view === 'grid' ? (
|
|
94
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-3">
|
|
95
|
+
{entries.map(entry => (
|
|
96
|
+
<Link
|
|
97
|
+
key={entry.path}
|
|
98
|
+
href={`/view/${encodePath(entry.path)}`}
|
|
99
|
+
className="flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center"
|
|
100
|
+
>
|
|
101
|
+
<FileIconLarge node={entry} />
|
|
102
|
+
<span className="text-xs text-foreground leading-snug line-clamp-2 w-full" suppressHydrationWarning>
|
|
103
|
+
{entry.name}
|
|
104
|
+
</span>
|
|
105
|
+
{entry.type === 'directory' && (
|
|
106
|
+
<span className="text-[10px] text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
107
|
+
)}
|
|
108
|
+
</Link>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<div className="flex flex-col divide-y divide-border border border-border rounded-xl overflow-hidden">
|
|
113
|
+
{entries.map(entry => (
|
|
114
|
+
<Link
|
|
115
|
+
key={entry.path}
|
|
116
|
+
href={`/view/${encodePath(entry.path)}`}
|
|
117
|
+
className="flex items-center gap-3 px-4 py-3 bg-card hover:bg-accent transition-colors duration-100"
|
|
118
|
+
>
|
|
119
|
+
<FileIcon node={entry} />
|
|
120
|
+
<span className="flex-1 text-sm text-foreground truncate" suppressHydrationWarning>
|
|
121
|
+
{entry.name}
|
|
122
|
+
</span>
|
|
123
|
+
{entry.type === 'directory' ? (
|
|
124
|
+
<span className="text-xs text-muted-foreground shrink-0">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
125
|
+
) : entry.mtime ? (
|
|
126
|
+
<span className="text-xs text-muted-foreground shrink-0 tabular-nums" style={{ fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
|
|
127
|
+
{new Date(entry.mtime).toLocaleDateString()}
|
|
128
|
+
</span>
|
|
129
|
+
) : null}
|
|
130
|
+
</Link>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { EditorView, basicSetup } from 'codemirror';
|
|
5
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
6
|
+
import { oneDark } from '@codemirror/theme-one-dark';
|
|
7
|
+
import { EditorState } from '@codemirror/state';
|
|
8
|
+
|
|
9
|
+
interface EditorProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
language?: 'markdown' | 'plain';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const darkTheme = EditorView.theme({
|
|
16
|
+
'&': {
|
|
17
|
+
backgroundColor: '#09090b',
|
|
18
|
+
height: '100%',
|
|
19
|
+
fontSize: '0.875rem',
|
|
20
|
+
fontFamily: 'var(--font-geist-mono), ui-monospace, monospace',
|
|
21
|
+
},
|
|
22
|
+
'.cm-scroller': {
|
|
23
|
+
overflow: 'auto',
|
|
24
|
+
lineHeight: '1.6',
|
|
25
|
+
},
|
|
26
|
+
'.cm-content': {
|
|
27
|
+
padding: '16px',
|
|
28
|
+
caretColor: '#60a5fa',
|
|
29
|
+
},
|
|
30
|
+
'.cm-focused': {
|
|
31
|
+
outline: 'none',
|
|
32
|
+
},
|
|
33
|
+
'.cm-line': {
|
|
34
|
+
padding: '0 4px',
|
|
35
|
+
},
|
|
36
|
+
'.cm-gutters': {
|
|
37
|
+
backgroundColor: '#0d0d0f',
|
|
38
|
+
borderRight: '1px solid #27272a',
|
|
39
|
+
color: '#52525b',
|
|
40
|
+
},
|
|
41
|
+
'.cm-activeLineGutter': {
|
|
42
|
+
backgroundColor: '#18181b',
|
|
43
|
+
},
|
|
44
|
+
'.cm-activeLine': {
|
|
45
|
+
backgroundColor: '#18181b50',
|
|
46
|
+
},
|
|
47
|
+
'.cm-selectionBackground': {
|
|
48
|
+
backgroundColor: '#2563eb40',
|
|
49
|
+
},
|
|
50
|
+
'&.cm-focused .cm-selectionBackground': {
|
|
51
|
+
backgroundColor: '#2563eb60',
|
|
52
|
+
},
|
|
53
|
+
'.cm-cursor': {
|
|
54
|
+
borderLeftColor: '#60a5fa',
|
|
55
|
+
borderLeftWidth: '2px',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default function Editor({ value, onChange, language = 'markdown' }: EditorProps) {
|
|
60
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
62
|
+
const onChangeRef = useRef(onChange);
|
|
63
|
+
onChangeRef.current = onChange;
|
|
64
|
+
|
|
65
|
+
// Track whether update is from external value change
|
|
66
|
+
const isExternalUpdate = useRef(false);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!containerRef.current) return;
|
|
70
|
+
|
|
71
|
+
const updateListener = EditorView.updateListener.of((update) => {
|
|
72
|
+
if (update.docChanged && !isExternalUpdate.current) {
|
|
73
|
+
onChangeRef.current(update.state.doc.toString());
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const state = EditorState.create({
|
|
78
|
+
doc: value,
|
|
79
|
+
extensions: [
|
|
80
|
+
basicSetup,
|
|
81
|
+
oneDark,
|
|
82
|
+
darkTheme,
|
|
83
|
+
language === 'markdown' ? markdown() : [],
|
|
84
|
+
updateListener,
|
|
85
|
+
EditorView.lineWrapping,
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const view = new EditorView({
|
|
90
|
+
state,
|
|
91
|
+
parent: containerRef.current,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
viewRef.current = view;
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
view.destroy();
|
|
98
|
+
viewRef.current = null;
|
|
99
|
+
};
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
// Sync external value changes to editor
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const view = viewRef.current;
|
|
106
|
+
if (!view) return;
|
|
107
|
+
const current = view.state.doc.toString();
|
|
108
|
+
if (current !== value) {
|
|
109
|
+
isExternalUpdate.current = true;
|
|
110
|
+
view.dispatch({
|
|
111
|
+
changes: { from: 0, to: current.length, insert: value },
|
|
112
|
+
});
|
|
113
|
+
isExternalUpdate.current = false;
|
|
114
|
+
}
|
|
115
|
+
}, [value]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
ref={containerRef}
|
|
120
|
+
className="h-full w-full overflow-hidden rounded-lg border border-zinc-800"
|
|
121
|
+
style={{ minHeight: '400px' }}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
|
+
|
|
5
|
+
const Editor = dynamic(() => import('./Editor'), {
|
|
6
|
+
ssr: false,
|
|
7
|
+
loading: () => (
|
|
8
|
+
<div className="h-full w-full min-h-[400px] rounded-lg border border-zinc-800 bg-zinc-900 flex items-center justify-center">
|
|
9
|
+
<div className="flex items-center gap-2 text-zinc-500 text-sm">
|
|
10
|
+
<div className="w-4 h-4 border-2 border-zinc-600 border-t-zinc-300 rounded-full animate-spin" />
|
|
11
|
+
<span>Loading editor...</span>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default Editor;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Component, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface State {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error?: Error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class ErrorBoundary extends Component<Props, State> {
|
|
16
|
+
constructor(props: Props) {
|
|
17
|
+
super(props);
|
|
18
|
+
this.state = { hasError: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromError(error: Error): State {
|
|
22
|
+
return { hasError: true, error };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
26
|
+
console.error('[ErrorBoundary]', error, info.componentStack);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render() {
|
|
30
|
+
if (this.state.hasError) {
|
|
31
|
+
if (this.props.fallback) return this.props.fallback;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-center justify-center min-h-[50vh] px-4">
|
|
35
|
+
<div className="text-center max-w-md">
|
|
36
|
+
<h2 className="text-lg font-semibold text-foreground mb-2">Something went wrong</h2>
|
|
37
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
38
|
+
{this.state.error?.message || 'An unexpected error occurred.'}
|
|
39
|
+
</p>
|
|
40
|
+
<button
|
|
41
|
+
onClick={() => this.setState({ hasError: false, error: undefined })}
|
|
42
|
+
className="px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors"
|
|
43
|
+
>
|
|
44
|
+
Try again
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return this.props.children;
|
|
52
|
+
}
|
|
53
|
+
}
|