@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,213 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
5
|
+
|
|
6
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface TimelineEntry {
|
|
9
|
+
heading: string;
|
|
10
|
+
date: Date | null;
|
|
11
|
+
body: string; // raw markdown lines joined
|
|
12
|
+
tags: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Detect date-like H2 headings: ## 2025-01-15, ## Jan 2025, ## 2025/01/15, etc.
|
|
16
|
+
const DATE_RE = /(\d{4}[-/]\d{1,2}(?:[-/]\d{1,2})?|[A-Za-z]+ \d{4}|\d{4}年\d{1,2}月(?:\d{1,2}日)?)/;
|
|
17
|
+
|
|
18
|
+
function parseDate(s: string): Date | null {
|
|
19
|
+
const m = DATE_RE.exec(s);
|
|
20
|
+
if (!m) return null;
|
|
21
|
+
const d = new Date(m[1].replace(/[/年月]/g, '-').replace('日', ''));
|
|
22
|
+
return isNaN(d.getTime()) ? null : d;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Extract #tag or **tag** markers from body text
|
|
26
|
+
function extractTags(body: string): string[] {
|
|
27
|
+
const tags: string[] = [];
|
|
28
|
+
const hashTags = body.match(/#([\w\u4e00-\u9fff]+)/g);
|
|
29
|
+
if (hashTags) tags.push(...hashTags.map(t => t.slice(1)));
|
|
30
|
+
return [...new Set(tags)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseTimeline(content: string): TimelineEntry[] {
|
|
34
|
+
const lines = content.split('\n');
|
|
35
|
+
const entries: TimelineEntry[] = [];
|
|
36
|
+
let current: TimelineEntry | null = null;
|
|
37
|
+
let bodyLines: string[] = [];
|
|
38
|
+
|
|
39
|
+
const flush = () => {
|
|
40
|
+
if (!current) return;
|
|
41
|
+
const body = bodyLines.join('\n').trim();
|
|
42
|
+
current.body = body;
|
|
43
|
+
current.tags = extractTags(body);
|
|
44
|
+
entries.push(current);
|
|
45
|
+
current = null;
|
|
46
|
+
bodyLines = [];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
// H1 is the document title — skip
|
|
51
|
+
if (/^# /.test(line)) continue;
|
|
52
|
+
|
|
53
|
+
// H2 = timeline entry
|
|
54
|
+
if (/^## /.test(line)) {
|
|
55
|
+
flush();
|
|
56
|
+
const heading = line.slice(3).trim();
|
|
57
|
+
current = { heading, date: parseDate(heading), body: '', tags: [] };
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (current) bodyLines.push(line);
|
|
62
|
+
}
|
|
63
|
+
flush();
|
|
64
|
+
|
|
65
|
+
return entries;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Markdown inline renderer (no extra dep) ──────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function renderInline(text: string): string {
|
|
71
|
+
return text
|
|
72
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
73
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
74
|
+
.replace(/`(.+?)`/g, '<code style="font-family:\'IBM Plex Mono\',monospace;font-size:0.85em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
|
|
75
|
+
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, target, alias) =>
|
|
76
|
+
`<span style="color:var(--amber);cursor:pointer" title="${target}">${alias ?? target}</span>`)
|
|
77
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:var(--amber)">$1</a>');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderBody(body: string): string {
|
|
81
|
+
const lines = body.split('\n');
|
|
82
|
+
const out: string[] = [];
|
|
83
|
+
let inList = false;
|
|
84
|
+
|
|
85
|
+
const closeList = () => { if (inList) { out.push('</ul>'); inList = false; } };
|
|
86
|
+
|
|
87
|
+
for (const raw of lines) {
|
|
88
|
+
const line = raw.trimEnd();
|
|
89
|
+
if (!line) { closeList(); out.push('<br/>'); continue; }
|
|
90
|
+
|
|
91
|
+
if (/^### /.test(line)) { closeList(); out.push(`<h3 style="font-size:0.8rem;font-weight:600;color:var(--muted-foreground);text-transform:uppercase;letter-spacing:.06em;margin:.9em 0 .3em">${renderInline(line.slice(4))}</h3>`); continue; }
|
|
92
|
+
if (/^- /.test(line) || /^\* /.test(line)) {
|
|
93
|
+
if (!inList) { out.push('<ul style="margin:.3em 0;padding-left:1.2em;list-style:disc">'); inList = true; }
|
|
94
|
+
out.push(`<li style="margin:.15em 0;font-size:.82rem;color:var(--foreground)">${renderInline(line.slice(2))}</li>`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (/^\d+\. /.test(line)) {
|
|
98
|
+
if (!inList) { out.push('<ol style="margin:.3em 0;padding-left:1.2em">'); inList = true; }
|
|
99
|
+
out.push(`<li style="margin:.15em 0;font-size:.82rem;color:var(--foreground)">${renderInline(line.replace(/^\d+\. /, ''))}</li>`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
closeList();
|
|
103
|
+
out.push(`<p style="margin:.25em 0;font-size:.82rem;line-height:1.6;color:var(--foreground)">${renderInline(line)}</p>`);
|
|
104
|
+
}
|
|
105
|
+
closeList();
|
|
106
|
+
return out.join('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Tag color ────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const TAG_PALETTE = [
|
|
112
|
+
{ bg: 'rgba(200,135,58,0.12)', text: 'var(--amber)' },
|
|
113
|
+
{ bg: 'rgba(122,173,128,0.12)', text: '#7aad80' },
|
|
114
|
+
{ bg: 'rgba(138,180,216,0.12)', text: '#8ab4d8' },
|
|
115
|
+
{ bg: 'rgba(200,160,216,0.12)', text: '#c8a0d8' },
|
|
116
|
+
];
|
|
117
|
+
function tagColor(tag: string) {
|
|
118
|
+
let h = 0;
|
|
119
|
+
for (let i = 0; i < tag.length; i++) h = (h * 31 + tag.charCodeAt(i)) & 0xffff;
|
|
120
|
+
return TAG_PALETTE[h % TAG_PALETTE.length];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatDate(d: Date): string {
|
|
124
|
+
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export function TimelineRenderer({ content }: RendererContext) {
|
|
130
|
+
const entries = useMemo(() => parseTimeline(content), [content]);
|
|
131
|
+
|
|
132
|
+
if (entries.length === 0) {
|
|
133
|
+
return (
|
|
134
|
+
<div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 13 }}>
|
|
135
|
+
No timeline entries found. Add <code style={{ background: 'var(--muted)', padding: '1px 6px', borderRadius: 4 }}>## 2025-01-15</code> headings to create entries.
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
|
|
142
|
+
{/* count pill */}
|
|
143
|
+
<div style={{ marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
144
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
|
|
145
|
+
{entries.length} {entries.length === 1 ? 'entry' : 'entries'}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* timeline */}
|
|
150
|
+
<div style={{ position: 'relative', paddingLeft: 28 }}>
|
|
151
|
+
{/* vertical line */}
|
|
152
|
+
<div style={{ position: 'absolute', left: 6, top: 8, bottom: 8, width: 1, background: 'var(--border)' }} />
|
|
153
|
+
|
|
154
|
+
{entries.map((entry, idx) => (
|
|
155
|
+
<div key={idx} style={{ position: 'relative', marginBottom: '1.5rem' }}>
|
|
156
|
+
{/* dot */}
|
|
157
|
+
<div style={{
|
|
158
|
+
position: 'absolute',
|
|
159
|
+
left: -22,
|
|
160
|
+
top: 10,
|
|
161
|
+
width: 9,
|
|
162
|
+
height: 9,
|
|
163
|
+
borderRadius: '50%',
|
|
164
|
+
background: entry.date ? 'var(--amber)' : 'var(--border)',
|
|
165
|
+
outline: entry.date ? '2px solid var(--amber-dim)' : 'none',
|
|
166
|
+
zIndex: 1,
|
|
167
|
+
}} />
|
|
168
|
+
|
|
169
|
+
{/* card */}
|
|
170
|
+
<div style={{
|
|
171
|
+
background: 'var(--card)',
|
|
172
|
+
border: '1px solid var(--border)',
|
|
173
|
+
borderRadius: 10,
|
|
174
|
+
padding: '14px 18px',
|
|
175
|
+
transition: 'border-color .15s',
|
|
176
|
+
}}>
|
|
177
|
+
{/* header */}
|
|
178
|
+
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
|
|
179
|
+
<span style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontWeight: 600, fontSize: '0.9rem', color: 'var(--foreground)' }}>
|
|
180
|
+
{entry.heading}
|
|
181
|
+
</span>
|
|
182
|
+
{entry.date && (
|
|
183
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: 'var(--muted-foreground)', opacity: 0.7, flexShrink: 0 }}>
|
|
184
|
+
{formatDate(entry.date)}
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* body */}
|
|
190
|
+
{entry.body && (
|
|
191
|
+
<div dangerouslySetInnerHTML={{ __html: renderBody(entry.body) }} />
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* tags */}
|
|
195
|
+
{entry.tags.length > 0 && (
|
|
196
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginTop: 10 }}>
|
|
197
|
+
{entry.tags.map(tag => {
|
|
198
|
+
const c = tagColor(tag);
|
|
199
|
+
return (
|
|
200
|
+
<span key={tag} style={{ fontSize: '0.68rem', padding: '1px 8px', borderRadius: 999, fontFamily: "'IBM Plex Mono',monospace", background: c.bg, color: c.text }}>
|
|
201
|
+
#{tag}
|
|
202
|
+
</span>
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
4
|
+
import { CheckSquare, Square, ChevronDown, ChevronRight, Plus, Trash2 } from 'lucide-react';
|
|
5
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
|
+
|
|
7
|
+
// ─── Parser ──────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
interface TodoItem {
|
|
10
|
+
id: string;
|
|
11
|
+
text: string;
|
|
12
|
+
checked: boolean;
|
|
13
|
+
indent: number;
|
|
14
|
+
lineIndex: number;
|
|
15
|
+
section: string;
|
|
16
|
+
children: TodoItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SectionMeta {
|
|
20
|
+
items: TodoItem[];
|
|
21
|
+
lastLineIndex: number; // line index of the last todo in this section (for append)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SECTION_RE = /^#{1,6}\s+(.+)/;
|
|
25
|
+
const TODO_RE = /^(\s*)- \[([ xX])\]\s+(.*)$/;
|
|
26
|
+
|
|
27
|
+
function parseMarkdownTodos(raw: string): {
|
|
28
|
+
sections: Record<string, SectionMeta>;
|
|
29
|
+
lines: string[];
|
|
30
|
+
} {
|
|
31
|
+
const lines = raw.split('\n');
|
|
32
|
+
let currentSection = 'General';
|
|
33
|
+
const sections: Record<string, SectionMeta> = {};
|
|
34
|
+
const stack: TodoItem[] = [];
|
|
35
|
+
|
|
36
|
+
lines.forEach((line, lineIndex) => {
|
|
37
|
+
const sectionMatch = line.match(SECTION_RE);
|
|
38
|
+
if (sectionMatch) {
|
|
39
|
+
currentSection = sectionMatch[1].replace(/^[^\w\s]+\s*/, '').trim();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const todoMatch = line.match(TODO_RE);
|
|
44
|
+
if (!todoMatch) return;
|
|
45
|
+
|
|
46
|
+
const [, indentStr, checkChar, rawText] = todoMatch;
|
|
47
|
+
const indent = Math.floor(indentStr.length / 2);
|
|
48
|
+
const checked = checkChar.toLowerCase() === 'x';
|
|
49
|
+
const text = rawText.replace(/\*\*(.*?)\*\*/g, '$1').trim();
|
|
50
|
+
|
|
51
|
+
const item: TodoItem = {
|
|
52
|
+
id: `${lineIndex}`,
|
|
53
|
+
text,
|
|
54
|
+
checked,
|
|
55
|
+
indent,
|
|
56
|
+
lineIndex,
|
|
57
|
+
section: currentSection,
|
|
58
|
+
children: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
62
|
+
stack.pop();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!sections[currentSection]) {
|
|
66
|
+
sections[currentSection] = { items: [], lastLineIndex: -1 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (stack.length > 0) {
|
|
70
|
+
stack[stack.length - 1].children.push(item);
|
|
71
|
+
} else {
|
|
72
|
+
sections[currentSection].items.push(item);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Track the furthest line in this section
|
|
76
|
+
sections[currentSection].lastLineIndex = Math.max(
|
|
77
|
+
sections[currentSection].lastLineIndex,
|
|
78
|
+
lineIndex,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
stack.push(item);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return { sections, lines };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function applyLines(lines: string[], ops: (l: string[]) => void): string {
|
|
88
|
+
const next = [...lines];
|
|
89
|
+
ops(next);
|
|
90
|
+
return next.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toggleInLines(lines: string[], lineIndex: number, checked: boolean): string {
|
|
94
|
+
return applyLines(lines, (l) => {
|
|
95
|
+
l[lineIndex] = checked
|
|
96
|
+
? l[lineIndex].replace(/- \[ \]/, '- [x]')
|
|
97
|
+
: l[lineIndex].replace(/- \[[xX]\]/, '- [ ]');
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renameInLines(lines: string[], lineIndex: number, newText: string): string {
|
|
102
|
+
return applyLines(lines, (l) => {
|
|
103
|
+
l[lineIndex] = l[lineIndex].replace(/(- \[[ xX]\]\s*)(.*)$/, `$1${newText}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deleteInLines(lines: string[], lineIndex: number): string {
|
|
108
|
+
return applyLines(lines, (l) => {
|
|
109
|
+
l.splice(lineIndex, 1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function addInLines(lines: string[], afterLineIndex: number, text: string): string {
|
|
114
|
+
return applyLines(lines, (l) => {
|
|
115
|
+
const insertAt = afterLineIndex < 0 ? l.length : afterLineIndex + 1;
|
|
116
|
+
l.splice(insertAt, 0, `- [ ] ${text}`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Components ──────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const SECTION_STYLES: Record<string, { dot: string; label: string }> = {
|
|
123
|
+
'TODAY': { dot: 'bg-red-400', label: 'text-red-400' },
|
|
124
|
+
'Workflows': { dot: 'bg-amber-400', label: 'text-amber-400' },
|
|
125
|
+
'Backlog': { dot: 'bg-blue-400', label: 'text-blue-400' },
|
|
126
|
+
'Maintenance': { dot: 'bg-zinc-400', label: 'text-zinc-400' },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function sectionStyle(name: string) {
|
|
130
|
+
const key = Object.keys(SECTION_STYLES).find(k => name.includes(k));
|
|
131
|
+
return key ? SECTION_STYLES[key] : { dot: 'bg-zinc-500', label: 'text-zinc-400' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function countDone(items: TodoItem[]): number {
|
|
135
|
+
return items.reduce((n, item) => n + (item.checked ? 1 : 0) + countDone(item.children), 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function countTotal(items: TodoItem[]): number {
|
|
139
|
+
return items.reduce((n, item) => n + 1 + countTotal(item.children), 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Inline editable text ────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
function InlineText({
|
|
145
|
+
text,
|
|
146
|
+
checked,
|
|
147
|
+
onRename,
|
|
148
|
+
}: {
|
|
149
|
+
text: string;
|
|
150
|
+
checked: boolean;
|
|
151
|
+
onRename: (newText: string) => void;
|
|
152
|
+
}) {
|
|
153
|
+
const [editing, setEditing] = useState(false);
|
|
154
|
+
const [value, setValue] = useState(text);
|
|
155
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
156
|
+
|
|
157
|
+
useEffect(() => { setValue(text); }, [text]);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (editing) inputRef.current?.select();
|
|
161
|
+
}, [editing]);
|
|
162
|
+
|
|
163
|
+
function commit() {
|
|
164
|
+
const trimmed = value.trim();
|
|
165
|
+
if (trimmed && trimmed !== text) onRename(trimmed);
|
|
166
|
+
else setValue(text);
|
|
167
|
+
setEditing(false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (editing) {
|
|
171
|
+
return (
|
|
172
|
+
<input
|
|
173
|
+
ref={inputRef}
|
|
174
|
+
value={value}
|
|
175
|
+
onChange={e => setValue(e.target.value)}
|
|
176
|
+
onBlur={commit}
|
|
177
|
+
onKeyDown={e => {
|
|
178
|
+
if (e.key === 'Enter') commit();
|
|
179
|
+
if (e.key === 'Escape') { setValue(text); setEditing(false); }
|
|
180
|
+
}}
|
|
181
|
+
className="flex-1 bg-transparent border-b text-sm leading-relaxed outline-none min-w-0"
|
|
182
|
+
style={{ borderColor: 'var(--amber)', color: 'var(--foreground)' }}
|
|
183
|
+
onClick={e => e.stopPropagation()}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<span
|
|
190
|
+
className={`flex-1 text-sm leading-relaxed cursor-text min-w-0 ${checked ? 'line-through text-muted-foreground' : 'text-foreground'}`}
|
|
191
|
+
onDoubleClick={() => setEditing(true)}
|
|
192
|
+
title="Double-click to edit"
|
|
193
|
+
>
|
|
194
|
+
{text}
|
|
195
|
+
</span>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Add item input ───────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function AddItemRow({ onAdd }: { onAdd: (text: string) => void }) {
|
|
202
|
+
const [active, setActive] = useState(false);
|
|
203
|
+
const [value, setValue] = useState('');
|
|
204
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (active) inputRef.current?.focus();
|
|
208
|
+
}, [active]);
|
|
209
|
+
|
|
210
|
+
function commit() {
|
|
211
|
+
const trimmed = value.trim();
|
|
212
|
+
if (trimmed) onAdd(trimmed);
|
|
213
|
+
setValue('');
|
|
214
|
+
setActive(false);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!active) {
|
|
218
|
+
return (
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => setActive(true)}
|
|
221
|
+
className="flex items-center gap-1.5 w-full px-3 py-1.5 rounded-lg text-xs transition-colors hover:bg-muted/60 mt-1"
|
|
222
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
223
|
+
>
|
|
224
|
+
<Plus size={12} />
|
|
225
|
+
<span>Add item</span>
|
|
226
|
+
</button>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex items-center gap-2 px-3 py-1.5 mt-1">
|
|
232
|
+
<span className="w-[13px] shrink-0" />
|
|
233
|
+
<span className="shrink-0" style={{ color: 'var(--muted-foreground)' }}>
|
|
234
|
+
<Square size={15} />
|
|
235
|
+
</span>
|
|
236
|
+
<input
|
|
237
|
+
ref={inputRef}
|
|
238
|
+
value={value}
|
|
239
|
+
onChange={e => setValue(e.target.value)}
|
|
240
|
+
onBlur={commit}
|
|
241
|
+
onKeyDown={e => {
|
|
242
|
+
if (e.key === 'Enter') commit();
|
|
243
|
+
if (e.key === 'Escape') { setValue(''); setActive(false); }
|
|
244
|
+
}}
|
|
245
|
+
placeholder="New item…"
|
|
246
|
+
className="flex-1 bg-transparent border-b text-sm outline-none"
|
|
247
|
+
style={{ borderColor: 'var(--amber)', color: 'var(--foreground)' }}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Todo item row ────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function TodoItemRow({
|
|
256
|
+
item,
|
|
257
|
+
depth,
|
|
258
|
+
onToggle,
|
|
259
|
+
onRename,
|
|
260
|
+
onDelete,
|
|
261
|
+
}: {
|
|
262
|
+
item: TodoItem;
|
|
263
|
+
depth: number;
|
|
264
|
+
onToggle: (lineIndex: number, checked: boolean) => void;
|
|
265
|
+
onRename: (lineIndex: number, newText: string) => void;
|
|
266
|
+
onDelete: (lineIndex: number) => void;
|
|
267
|
+
}) {
|
|
268
|
+
const [open, setOpen] = useState(true);
|
|
269
|
+
const hasChildren = item.children.length > 0;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div>
|
|
273
|
+
<div
|
|
274
|
+
className={`group flex items-center gap-2 py-1.5 px-2 rounded-lg transition-colors hover:bg-muted/60 ${item.checked ? 'opacity-50' : ''}`}
|
|
275
|
+
style={{ paddingLeft: `${8 + depth * 20}px` }}
|
|
276
|
+
>
|
|
277
|
+
{hasChildren ? (
|
|
278
|
+
<button
|
|
279
|
+
onClick={() => setOpen(v => !v)}
|
|
280
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
281
|
+
>
|
|
282
|
+
{open ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
|
283
|
+
</button>
|
|
284
|
+
) : (
|
|
285
|
+
<span className="w-[13px] shrink-0" />
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
<button
|
|
289
|
+
onClick={() => onToggle(item.lineIndex, !item.checked)}
|
|
290
|
+
className="shrink-0 transition-colors"
|
|
291
|
+
style={{ color: item.checked ? 'var(--amber)' : 'var(--muted-foreground)' }}
|
|
292
|
+
>
|
|
293
|
+
{item.checked ? <CheckSquare size={15} /> : <Square size={15} />}
|
|
294
|
+
</button>
|
|
295
|
+
|
|
296
|
+
<InlineText
|
|
297
|
+
text={item.text}
|
|
298
|
+
checked={item.checked}
|
|
299
|
+
onRename={(newText) => onRename(item.lineIndex, newText)}
|
|
300
|
+
/>
|
|
301
|
+
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => onDelete(item.lineIndex)}
|
|
304
|
+
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-destructive/10"
|
|
305
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
306
|
+
title="Delete item"
|
|
307
|
+
>
|
|
308
|
+
<Trash2 size={12} />
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{hasChildren && open && (
|
|
313
|
+
<div>
|
|
314
|
+
{item.children.map(child => (
|
|
315
|
+
<TodoItemRow
|
|
316
|
+
key={child.id}
|
|
317
|
+
item={child}
|
|
318
|
+
depth={depth + 1}
|
|
319
|
+
onToggle={onToggle}
|
|
320
|
+
onRename={onRename}
|
|
321
|
+
onDelete={onDelete}
|
|
322
|
+
/>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Section card ─────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
function SectionCard({
|
|
333
|
+
name,
|
|
334
|
+
meta,
|
|
335
|
+
onToggle,
|
|
336
|
+
onRename,
|
|
337
|
+
onDelete,
|
|
338
|
+
onAdd,
|
|
339
|
+
}: {
|
|
340
|
+
name: string;
|
|
341
|
+
meta: SectionMeta;
|
|
342
|
+
onToggle: (lineIndex: number, checked: boolean) => void;
|
|
343
|
+
onRename: (lineIndex: number, newText: string) => void;
|
|
344
|
+
onDelete: (lineIndex: number) => void;
|
|
345
|
+
onAdd: (afterLineIndex: number, text: string) => void;
|
|
346
|
+
}) {
|
|
347
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
348
|
+
const style = sectionStyle(name);
|
|
349
|
+
const { items, lastLineIndex } = meta;
|
|
350
|
+
const done = countDone(items);
|
|
351
|
+
const total = countTotal(items);
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className="border border-border rounded-xl overflow-hidden bg-card">
|
|
355
|
+
<button
|
|
356
|
+
onClick={() => setCollapsed(v => !v)}
|
|
357
|
+
className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/40 transition-colors"
|
|
358
|
+
>
|
|
359
|
+
<div className="flex items-center gap-2">
|
|
360
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${style.dot}`} />
|
|
361
|
+
<span className={`text-xs font-semibold uppercase tracking-wider ${style.label}`} style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
362
|
+
{name}
|
|
363
|
+
</span>
|
|
364
|
+
<span className="text-xs text-muted-foreground">{done}/{total}</span>
|
|
365
|
+
</div>
|
|
366
|
+
{collapsed ? <ChevronRight size={14} className="text-muted-foreground" /> : <ChevronDown size={14} className="text-muted-foreground" />}
|
|
367
|
+
</button>
|
|
368
|
+
|
|
369
|
+
<div className="h-0.5 bg-muted mx-4">
|
|
370
|
+
<div
|
|
371
|
+
className="h-full rounded-full transition-all duration-500"
|
|
372
|
+
style={{ width: total ? `${(done / total) * 100}%` : '0%', background: 'var(--amber)' }}
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{!collapsed && (
|
|
377
|
+
<div className="px-2 py-2">
|
|
378
|
+
{items.map(item => (
|
|
379
|
+
<TodoItemRow
|
|
380
|
+
key={item.id}
|
|
381
|
+
item={item}
|
|
382
|
+
depth={0}
|
|
383
|
+
onToggle={onToggle}
|
|
384
|
+
onRename={onRename}
|
|
385
|
+
onDelete={onDelete}
|
|
386
|
+
/>
|
|
387
|
+
))}
|
|
388
|
+
{items.length === 0 && (
|
|
389
|
+
<p className="text-xs text-muted-foreground px-4 py-2">No items</p>
|
|
390
|
+
)}
|
|
391
|
+
<AddItemRow onAdd={(text) => onAdd(lastLineIndex, text)} />
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ─── Main Renderer ────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
export function TodoRenderer({ content, saveAction }: RendererContext) {
|
|
401
|
+
const [localContent, setLocalContent] = useState(content);
|
|
402
|
+
|
|
403
|
+
const { sections, lines } = useMemo(
|
|
404
|
+
() => parseMarkdownTodos(localContent),
|
|
405
|
+
[localContent],
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const totalDone = useMemo(
|
|
409
|
+
() => Object.values(sections).reduce((n, m) => n + countDone(m.items), 0),
|
|
410
|
+
[sections],
|
|
411
|
+
);
|
|
412
|
+
const totalItems = useMemo(
|
|
413
|
+
() => Object.values(sections).reduce((n, m) => n + countTotal(m.items), 0),
|
|
414
|
+
[sections],
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const persist = useCallback(async (next: string) => {
|
|
418
|
+
setLocalContent(next);
|
|
419
|
+
await saveAction(next);
|
|
420
|
+
}, [saveAction]);
|
|
421
|
+
|
|
422
|
+
const handleToggle = useCallback(async (lineIndex: number, checked: boolean) => {
|
|
423
|
+
await persist(toggleInLines(lines, lineIndex, checked));
|
|
424
|
+
}, [lines, persist]);
|
|
425
|
+
|
|
426
|
+
const handleRename = useCallback(async (lineIndex: number, newText: string) => {
|
|
427
|
+
await persist(renameInLines(lines, lineIndex, newText));
|
|
428
|
+
}, [lines, persist]);
|
|
429
|
+
|
|
430
|
+
const handleDelete = useCallback(async (lineIndex: number) => {
|
|
431
|
+
await persist(deleteInLines(lines, lineIndex));
|
|
432
|
+
}, [lines, persist]);
|
|
433
|
+
|
|
434
|
+
const handleAdd = useCallback(async (afterLineIndex: number, text: string) => {
|
|
435
|
+
await persist(addInLines(lines, afterLineIndex, text));
|
|
436
|
+
}, [lines, persist]);
|
|
437
|
+
|
|
438
|
+
const sectionEntries = Object.entries(sections);
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<div className="max-w-[900px] mx-auto xl:mr-[220px] px-0 py-2">
|
|
442
|
+
{/* Summary header */}
|
|
443
|
+
<div className="mb-6">
|
|
444
|
+
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
445
|
+
{totalDone} / {totalItems} completed
|
|
446
|
+
</p>
|
|
447
|
+
<div className="mt-1.5 w-48 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
448
|
+
<div
|
|
449
|
+
className="h-full rounded-full transition-all duration-500"
|
|
450
|
+
style={{ width: totalItems ? `${(totalDone / totalItems) * 100}%` : '0%', background: 'var(--amber)' }}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{/* Sections */}
|
|
456
|
+
<div className="flex flex-col gap-4">
|
|
457
|
+
{sectionEntries.map(([name, meta]) => (
|
|
458
|
+
<SectionCard
|
|
459
|
+
key={name}
|
|
460
|
+
name={name}
|
|
461
|
+
meta={meta}
|
|
462
|
+
onToggle={handleToggle}
|
|
463
|
+
onRename={handleRename}
|
|
464
|
+
onDelete={handleDelete}
|
|
465
|
+
onAdd={handleAdd}
|
|
466
|
+
/>
|
|
467
|
+
))}
|
|
468
|
+
{sectionEntries.length === 0 && (
|
|
469
|
+
<p className="text-sm text-muted-foreground">No TODO items found in this file.</p>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|