@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.
Files changed (282) hide show
  1. package/.env.local.example +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +423 -0
  4. package/README_zh.md +423 -0
  5. package/app/README.md +152 -0
  6. package/app/app/api/ask/route.ts +170 -0
  7. package/app/app/api/ask-sessions/route.ts +90 -0
  8. package/app/app/api/auth/route.ts +37 -0
  9. package/app/app/api/backlinks/route.ts +22 -0
  10. package/app/app/api/bootstrap/route.ts +37 -0
  11. package/app/app/api/extract-pdf/route.ts +82 -0
  12. package/app/app/api/file/route.ts +138 -0
  13. package/app/app/api/files/route.ts +12 -0
  14. package/app/app/api/git/route.ts +42 -0
  15. package/app/app/api/graph/route.ts +113 -0
  16. package/app/app/api/recent-files/route.ts +10 -0
  17. package/app/app/api/search/route.ts +17 -0
  18. package/app/app/api/settings/reset-token/route.ts +21 -0
  19. package/app/app/api/settings/route.ts +123 -0
  20. package/app/app/error.tsx +33 -0
  21. package/app/app/globals.css +368 -0
  22. package/app/app/icon.svg +35 -0
  23. package/app/app/layout.tsx +103 -0
  24. package/app/app/login/page.tsx +120 -0
  25. package/app/app/page.tsx +12 -0
  26. package/app/app/view/[...path]/ViewPageClient.tsx +343 -0
  27. package/app/app/view/[...path]/error.tsx +33 -0
  28. package/app/app/view/[...path]/loading.tsx +15 -0
  29. package/app/app/view/[...path]/page.tsx +93 -0
  30. package/app/components/AskFab.tsx +59 -0
  31. package/app/components/AskModal.tsx +398 -0
  32. package/app/components/Backlinks.tsx +75 -0
  33. package/app/components/Breadcrumb.tsx +31 -0
  34. package/app/components/CsvView.tsx +325 -0
  35. package/app/components/DirView.tsx +138 -0
  36. package/app/components/Editor.tsx +124 -0
  37. package/app/components/EditorWrapper.tsx +17 -0
  38. package/app/components/ErrorBoundary.tsx +53 -0
  39. package/app/components/FileTree.tsx +369 -0
  40. package/app/components/HomeContent.tsx +262 -0
  41. package/app/components/JsonView.tsx +27 -0
  42. package/app/components/MarkdownEditor.tsx +95 -0
  43. package/app/components/MarkdownView.tsx +118 -0
  44. package/app/components/SearchModal.tsx +193 -0
  45. package/app/components/SettingsModal.tsx +237 -0
  46. package/app/components/Sidebar.tsx +136 -0
  47. package/app/components/SidebarLayout.tsx +36 -0
  48. package/app/components/TableOfContents.tsx +150 -0
  49. package/app/components/ThemeToggle.tsx +34 -0
  50. package/app/components/WysiwygEditor.tsx +75 -0
  51. package/app/components/ask/FileChip.tsx +30 -0
  52. package/app/components/ask/MentionPopover.tsx +52 -0
  53. package/app/components/ask/MessageList.tsx +126 -0
  54. package/app/components/ask/SessionHistory.tsx +49 -0
  55. package/app/components/renderers/AgentInspectorRenderer.tsx +277 -0
  56. package/app/components/renderers/BacklinksRenderer.tsx +147 -0
  57. package/app/components/renderers/ConfigRenderer.tsx +236 -0
  58. package/app/components/renderers/CsvRenderer.tsx +77 -0
  59. package/app/components/renderers/DiffRenderer.tsx +310 -0
  60. package/app/components/renderers/GraphRenderer.tsx +428 -0
  61. package/app/components/renderers/SummaryRenderer.tsx +251 -0
  62. package/app/components/renderers/TimelineRenderer.tsx +213 -0
  63. package/app/components/renderers/TodoRenderer.tsx +474 -0
  64. package/app/components/renderers/WorkflowRenderer.tsx +404 -0
  65. package/app/components/renderers/csv/BoardView.tsx +146 -0
  66. package/app/components/renderers/csv/ConfigPanel.tsx +117 -0
  67. package/app/components/renderers/csv/EditableCell.tsx +43 -0
  68. package/app/components/renderers/csv/GalleryView.tsx +40 -0
  69. package/app/components/renderers/csv/TableView.tsx +164 -0
  70. package/app/components/renderers/csv/types.ts +87 -0
  71. package/app/components/settings/AiTab.tsx +111 -0
  72. package/app/components/settings/AppearanceTab.tsx +101 -0
  73. package/app/components/settings/KnowledgeTab.tsx +157 -0
  74. package/app/components/settings/PluginsTab.tsx +82 -0
  75. package/app/components/settings/Primitives.tsx +60 -0
  76. package/app/components/settings/ShortcutsTab.tsx +22 -0
  77. package/app/components/settings/types.ts +41 -0
  78. package/app/components/ui/button.tsx +60 -0
  79. package/app/components/ui/dialog.tsx +157 -0
  80. package/app/components/ui/input.tsx +20 -0
  81. package/app/components/ui/scroll-area.tsx +55 -0
  82. package/app/components/ui/toggle.tsx +44 -0
  83. package/app/components/ui/tooltip.tsx +66 -0
  84. package/app/components.json +25 -0
  85. package/app/data/pages/home-dark.png +0 -0
  86. package/app/data/pages/home-mobile-crop.png +0 -0
  87. package/app/data/pages/home-mobile.png +0 -0
  88. package/app/data/pages/home.png +0 -0
  89. package/app/data/pages/view-dir.png +0 -0
  90. package/app/data/pages/view-file-bot.png +0 -0
  91. package/app/data/pages/view-file-dark-crop.png +0 -0
  92. package/app/data/pages/view-file-dark.png +0 -0
  93. package/app/data/pages/view-file-mobile.png +0 -0
  94. package/app/data/pages/view-file-sm.png +0 -0
  95. package/app/data/pages/view-file-top.png +0 -0
  96. package/app/data/pages/view-file.png +0 -0
  97. package/app/eslint.config.mjs +18 -0
  98. package/app/hooks/useAskSession.ts +181 -0
  99. package/app/hooks/useFileUpload.ts +126 -0
  100. package/app/hooks/useMention.ts +65 -0
  101. package/app/lib/LocaleContext.tsx +40 -0
  102. package/app/lib/actions.ts +40 -0
  103. package/app/lib/agent/index.ts +3 -0
  104. package/app/lib/agent/model.ts +18 -0
  105. package/app/lib/agent/prompt.ts +32 -0
  106. package/app/lib/agent/tools.ts +151 -0
  107. package/app/lib/api.ts +55 -0
  108. package/app/lib/core/backlinks.ts +40 -0
  109. package/app/lib/core/csv.ts +28 -0
  110. package/app/lib/core/fs-ops.ts +118 -0
  111. package/app/lib/core/git.ts +50 -0
  112. package/app/lib/core/index.ts +58 -0
  113. package/app/lib/core/lines.ts +89 -0
  114. package/app/lib/core/search.ts +79 -0
  115. package/app/lib/core/security.ts +43 -0
  116. package/app/lib/core/tree.ts +113 -0
  117. package/app/lib/core/types.ts +40 -0
  118. package/app/lib/fs.ts +467 -0
  119. package/app/lib/i18n.ts +300 -0
  120. package/app/lib/jwt.ts +58 -0
  121. package/app/lib/renderers/index.ts +79 -0
  122. package/app/lib/renderers/registry.ts +70 -0
  123. package/app/lib/settings.ts +150 -0
  124. package/app/lib/types.ts +32 -0
  125. package/app/lib/utils.ts +34 -0
  126. package/app/next-env.d.ts +6 -0
  127. package/app/next.config.ts +10 -0
  128. package/app/package-lock.json +15306 -0
  129. package/app/package.json +71 -0
  130. package/app/postcss.config.mjs +7 -0
  131. package/app/proxy.ts +64 -0
  132. package/app/public/file.svg +1 -0
  133. package/app/public/globe.svg +1 -0
  134. package/app/public/landing/index.html +353 -0
  135. package/app/public/landing/style.css +216 -0
  136. package/app/public/logo-square.svg +37 -0
  137. package/app/public/logo.svg +37 -0
  138. package/app/public/next.svg +1 -0
  139. package/app/public/vercel.svg +1 -0
  140. package/app/public/window.svg +1 -0
  141. package/app/scripts/extract-pdf.cjs +56 -0
  142. package/app/tsconfig.json +34 -0
  143. package/app/vitest.config.ts +14 -0
  144. package/assets/demo-flow-zh.html +622 -0
  145. package/assets/images/demo-flow-dark.png +0 -0
  146. package/assets/images/demo-flow-light.png +0 -0
  147. package/assets/images/demo-flow-zh-dark.png +0 -0
  148. package/assets/images/demo-flow-zh-light.png +0 -0
  149. package/assets/images/gui-sync-cv.png +0 -0
  150. package/assets/logo-square.svg +37 -0
  151. package/bin/cli.js +894 -0
  152. package/mcp/README.md +113 -0
  153. package/mcp/package-lock.json +1717 -0
  154. package/mcp/package.json +18 -0
  155. package/mcp/src/index.ts +494 -0
  156. package/mcp/tsconfig.json +13 -0
  157. package/package.json +49 -0
  158. package/scripts/setup.js +675 -0
  159. package/scripts/upgrade-prompt.md +147 -0
  160. package/skills/mindos/SKILL.md +319 -0
  161. package/skills/mindos-zh/SKILL.md +318 -0
  162. package/templates/README.md +31 -0
  163. package/templates/empty/CHANGELOG.md +9 -0
  164. package/templates/empty/CONFIG.json +197 -0
  165. package/templates/empty/CONFIG.md +73 -0
  166. package/templates/empty/INSTRUCTION.md +177 -0
  167. package/templates/empty/README.md +27 -0
  168. package/templates/en/CHANGELOG.md +9 -0
  169. package/templates/en/CONFIG.json +197 -0
  170. package/templates/en/CONFIG.md +73 -0
  171. package/templates/en/INSTRUCTION.md +177 -0
  172. package/templates/en/README.md +27 -0
  173. package/templates/en/TODO.md +13 -0
  174. package/templates/en//360/237/221/244 Profile/INSTRUCTION.md" +21 -0
  175. package/templates/en//360/237/221/244 Profile/README.md" +15 -0
  176. package/templates/en//360/237/221/244 Profile//342/232/231/357/270/217 Preferences.md" +21 -0
  177. package/templates/en//360/237/221/244 Profile//360/237/216/257 Focus.md" +31 -0
  178. package/templates/en//360/237/221/244 Profile//360/237/221/244 Identity.md" +22 -0
  179. package/templates/en//360/237/223/232 Resources/INSTRUCTION.md" +29 -0
  180. package/templates/en//360/237/223/232 Resources/README.md" +21 -0
  181. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +1 -0
  182. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +1 -0
  183. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +1 -0
  184. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +1 -0
  185. package/templates/en//360/237/223/235 Notes/INSTRUCTION.md" +31 -0
  186. package/templates/en//360/237/223/235 Notes/Ideas/README.md" +8 -0
  187. package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +16 -0
  188. package/templates/en//360/237/223/235 Notes/Inbox/README.md" +8 -0
  189. package/templates/en//360/237/223/235 Notes/Inbox//360/237/247/252_example_quick_capture.md" +14 -0
  190. package/templates/en//360/237/223/235 Notes/Meetings/README.md" +8 -0
  191. package/templates/en//360/237/223/235 Notes/Meetings//360/237/247/252_example_meeting_note.md" +17 -0
  192. package/templates/en//360/237/223/235 Notes/README.md" +24 -0
  193. package/templates/en//360/237/223/235 Notes/Waiting/README.md" +8 -0
  194. package/templates/en//360/237/223/235 Notes/Waiting//360/237/247/252_example_blocked_item.md" +16 -0
  195. package/templates/en//360/237/224/204 Workflows/Configurations/README.md" +3 -0
  196. package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +14 -0
  197. package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +21 -0
  198. package/templates/en//360/237/224/204 Workflows/Information/README.md" +16 -0
  199. package/templates/en//360/237/224/204 Workflows/Information//360/237/247/252_example_info_capture_sop.md" +13 -0
  200. package/templates/en//360/237/224/204 Workflows/Media/README.md" +16 -0
  201. package/templates/en//360/237/224/204 Workflows/Media//360/237/247/252_example_content_publish_sop.md" +13 -0
  202. package/templates/en//360/237/224/204 Workflows/README.md" +22 -0
  203. package/templates/en//360/237/224/204 Workflows/Research/README.md" +16 -0
  204. package/templates/en//360/237/224/204 Workflows/Research//360/237/247/252_example_lit_review_sop.md" +16 -0
  205. package/templates/en//360/237/224/204 Workflows/Startup/README.md" +3 -0
  206. package/templates/en//360/237/224/204 Workflows/Startup//360/237/247/252_example_weekly_founder_ops.md" +22 -0
  207. package/templates/en//360/237/224/227 Connections/Classmates/README.md" +11 -0
  208. package/templates/en//360/237/224/227 Connections/Classmates//360/237/247/252_example_leo_chen.md" +16 -0
  209. package/templates/en//360/237/224/227 Connections/Colleagues/README.md" +11 -0
  210. package/templates/en//360/237/224/227 Connections/Colleagues//360/237/247/252_example_ethan_zhao.md" +16 -0
  211. package/templates/en//360/237/224/227 Connections/Connections Overview.csv" +5 -0
  212. package/templates/en//360/237/224/227 Connections/Family/README.md" +11 -0
  213. package/templates/en//360/237/224/227 Connections/Family//360/237/247/252_example_james_wang.md" +16 -0
  214. package/templates/en//360/237/224/227 Connections/Friends/README.md" +11 -0
  215. package/templates/en//360/237/224/227 Connections/Friends//360/237/247/252_example_lily_lin.md" +16 -0
  216. package/templates/en//360/237/224/227 Connections/INSTRUCTION.md" +56 -0
  217. package/templates/en//360/237/224/227 Connections/README.md" +20 -0
  218. package/templates/en//360/237/232/200 Projects/Archived/README.md" +9 -0
  219. package/templates/en//360/237/232/200 Projects/Archived//360/237/247/252_example_archived_project_note.md" +14 -0
  220. package/templates/en//360/237/232/200 Projects/INSTRUCTION.md" +29 -0
  221. package/templates/en//360/237/232/200 Projects/Products/README.md" +16 -0
  222. package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +20 -0
  223. package/templates/en//360/237/232/200 Projects/README.md" +21 -0
  224. package/templates/en//360/237/232/200 Projects/Research/README.md" +16 -0
  225. package/templates/en//360/237/232/200 Projects/Research//360/237/247/252_example_research_project_brief.md" +16 -0
  226. package/templates/template-generation-skill.md +79 -0
  227. package/templates/zh/CHANGELOG.md +9 -0
  228. package/templates/zh/CONFIG.json +197 -0
  229. package/templates/zh/CONFIG.md +66 -0
  230. package/templates/zh/INSTRUCTION.md +177 -0
  231. package/templates/zh/README.md +27 -0
  232. package/templates/zh/TODO.md +13 -0
  233. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/INSTRUCTION.md" +28 -0
  234. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +20 -0
  235. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/217 Preferences.md" +21 -0
  236. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/257 Focus.md" +31 -0
  237. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/244 Identity.md" +22 -0
  238. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/INSTRUCTION.md" +29 -0
  239. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +21 -0
  240. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +1 -0
  241. 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
  242. 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
  243. 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
  244. package/templates/zh//360/237/223/235 /347/254/224/350/256/260/INSTRUCTION.md" +31 -0
  245. package/templates/zh//360/237/223/235 /347/254/224/350/256/260/README.md" +24 -0
  246. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256/README.md" +8 -0
  247. 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
  248. 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
  249. 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
  250. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225/README.md" +8 -0
  251. 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
  252. 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
  253. 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
  254. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/INSTRUCTION.md" +29 -0
  255. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +21 -0
  256. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257/README.md" +16 -0
  257. 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
  258. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223/README.md" +16 -0
  259. 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
  260. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224/README.md" +16 -0
  261. 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
  262. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256/README.md" +3 -0
  263. 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
  264. package/templates/zh//360/237/224/227 /345/205/263/347/263/273/INSTRUCTION.md" +62 -0
  265. package/templates/zh//360/237/224/227 /345/205/263/347/263/273/README.md" +20 -0
  266. 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
  267. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213/README.md" +11 -0
  268. 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
  269. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246/README.md" +11 -0
  270. 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
  271. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272/README.md" +11 -0
  272. 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
  273. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213/README.md" +11 -0
  274. 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
  275. package/templates/zh//360/237/232/200 /351/241/271/347/233/256/INSTRUCTION.md" +31 -0
  276. package/templates/zh//360/237/232/200 /351/241/271/347/233/256/README.md" +21 -0
  277. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201/README.md" +16 -0
  278. 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
  279. 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
  280. 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
  281. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224/README.md" +16 -0
  282. 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,369 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef, useTransition, useEffect } from 'react';
4
+ import { useRouter, usePathname } from 'next/navigation';
5
+ import { FileNode } from '@/lib/types';
6
+ import { encodePath } from '@/lib/utils';
7
+ import { ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2, Trash2, Pencil } from 'lucide-react';
8
+ import { createFileAction, deleteFileAction, renameFileAction } from '@/lib/actions';
9
+ import { useLocale } from '@/lib/LocaleContext';
10
+
11
+ interface FileTreeProps {
12
+ nodes: FileNode[];
13
+ depth?: number;
14
+ onNavigate?: () => void;
15
+ }
16
+
17
+ function getIcon(node: FileNode) {
18
+ if (node.type === 'directory') return null;
19
+ if (node.extension === '.csv') return <Table size={14} className="text-emerald-400 shrink-0" />;
20
+ return <FileText size={14} className="text-zinc-400 shrink-0" />;
21
+ }
22
+
23
+ function getCurrentFilePath(pathname: string): string {
24
+ const prefix = '/view/';
25
+ if (!pathname.startsWith(prefix)) return '';
26
+ const encoded = pathname.slice(prefix.length);
27
+ return encoded.split('/').map(decodeURIComponent).join('/');
28
+ }
29
+
30
+ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: number; onDone: () => void }) {
31
+ const [value, setValue] = useState('');
32
+ const [isPending, startTransition] = useTransition();
33
+ const [error, setError] = useState('');
34
+ const router = useRouter();
35
+ const { t } = useLocale();
36
+
37
+ const handleSubmit = useCallback(() => {
38
+ const name = value.trim();
39
+ if (!name) { setError(t.fileTree.enterFileName); return; }
40
+ startTransition(async () => {
41
+ const result = await createFileAction(dirPath, name);
42
+ if (result.success && result.filePath) {
43
+ onDone();
44
+ router.push(`/view/${encodePath(result.filePath)}`);
45
+ router.refresh();
46
+ } else {
47
+ setError(result.error || t.fileTree.failed);
48
+ }
49
+ });
50
+ }, [value, dirPath, onDone, router, t]);
51
+
52
+ return (
53
+ <div className="px-2 pb-1" style={{ paddingLeft: `${depth * 12 + 20}px` }}>
54
+ <div className="flex items-center gap-1">
55
+ <input
56
+ autoFocus
57
+ type="text"
58
+ value={value}
59
+ onChange={(e) => { setValue(e.target.value); setError(''); }}
60
+ onKeyDown={(e) => {
61
+ if (e.key === 'Enter') handleSubmit();
62
+ if (e.key === 'Escape') onDone();
63
+ }}
64
+ placeholder="filename.md"
65
+ className="
66
+ flex-1 bg-muted border border-border rounded px-2 py-1
67
+ text-xs text-foreground placeholder:text-muted-foreground
68
+ focus:outline-none focus:border-blue-500/60
69
+ "
70
+ />
71
+ {isPending
72
+ ? <Loader2 size={13} className="text-zinc-500 animate-spin shrink-0" />
73
+ : (
74
+ <button
75
+ onClick={handleSubmit}
76
+ className="text-xs text-blue-400 hover:text-blue-300 shrink-0 px-1"
77
+ >
78
+ {t.fileTree.create}
79
+ </button>
80
+ )
81
+ }
82
+ </div>
83
+ {error && <p className="text-xs text-red-400 mt-0.5 px-1">{error}</p>}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ function DirectoryNode({ node, depth, currentPath, onNavigate }: {
89
+ node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
90
+ }) {
91
+ const router = useRouter();
92
+ const isActive = currentPath.startsWith(node.path + '/') || currentPath === node.path;
93
+ const [open, setOpen] = useState(depth === 0 ? true : isActive);
94
+ const [showNewFile, setShowNewFile] = useState(false);
95
+ const [renaming, setRenaming] = useState(false);
96
+ const [renameValue, setRenameValue] = useState(node.name);
97
+ const [isPending, startTransition] = useTransition();
98
+ const renameRef = useRef<HTMLInputElement>(null);
99
+ const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
100
+ const { t } = useLocale();
101
+
102
+ const toggle = useCallback(() => setOpen(v => !v), []);
103
+
104
+ useEffect(() => {
105
+ return () => {
106
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
107
+ };
108
+ }, []);
109
+
110
+ const startRename = useCallback((e?: React.MouseEvent) => {
111
+ e?.preventDefault();
112
+ e?.stopPropagation();
113
+ if (clickTimerRef.current) {
114
+ clearTimeout(clickTimerRef.current);
115
+ clickTimerRef.current = null;
116
+ }
117
+ setRenameValue(node.name);
118
+ setRenaming(true);
119
+ setTimeout(() => renameRef.current?.select(), 0);
120
+ }, [node.name]);
121
+
122
+ const commitRename = useCallback(() => {
123
+ const newName = renameValue.trim();
124
+ if (!newName || newName === node.name) { setRenaming(false); return; }
125
+ startTransition(async () => {
126
+ const result = await renameFileAction(node.path, newName);
127
+ if (result.success && result.newPath) {
128
+ setRenaming(false);
129
+ router.push(`/view/${encodePath(result.newPath)}`);
130
+ router.refresh();
131
+ } else {
132
+ setRenaming(false);
133
+ }
134
+ });
135
+ }, [renameValue, node.name, node.path, router]);
136
+
137
+ const handleSingleClick = useCallback(() => {
138
+ if (renaming) return;
139
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
140
+ clickTimerRef.current = setTimeout(() => {
141
+ router.push(`/view/${encodePath(node.path)}`);
142
+ onNavigate?.();
143
+ clickTimerRef.current = null;
144
+ }, 180);
145
+ }, [renaming, router, node.path, onNavigate]);
146
+
147
+ const handleDoubleClick = useCallback((e: React.MouseEvent) => {
148
+ startRename(e);
149
+ }, [startRename]);
150
+
151
+ if (renaming) {
152
+ return (
153
+ <div className="relative px-2 py-0.5" style={{ paddingLeft: `${depth * 12 + 8}px` }}>
154
+ <input
155
+ ref={renameRef}
156
+ autoFocus
157
+ value={renameValue}
158
+ onChange={e => setRenameValue(e.target.value)}
159
+ onKeyDown={e => {
160
+ if (e.key === 'Enter') commitRename();
161
+ if (e.key === 'Escape') setRenaming(false);
162
+ }}
163
+ onBlur={commitRename}
164
+ className="w-full bg-muted border border-blue-500/60 rounded px-2 py-0.5 text-xs text-foreground focus:outline-none"
165
+ />
166
+ {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
167
+ </div>
168
+ );
169
+ }
170
+
171
+ return (
172
+ <div>
173
+ <div className="relative group/dir flex items-center">
174
+ <button
175
+ onClick={toggle}
176
+ className="shrink-0 p-1 rounded hover:bg-muted text-zinc-500 transition-colors"
177
+ style={{ marginLeft: `${depth * 12 + 4}px` }}
178
+ aria-label={open ? 'Collapse' : 'Expand'}
179
+ >
180
+ <span className="block transition-transform duration-150" style={{ transform: open ? 'rotate(0deg)' : 'rotate(-90deg)' }}>
181
+ <ChevronDown size={13} />
182
+ </span>
183
+ </button>
184
+ <button
185
+ type="button"
186
+ onClick={handleSingleClick}
187
+ onDoubleClick={handleDoubleClick}
188
+ className={`
189
+ flex-1 flex items-center gap-1.5 px-1 py-1 rounded text-left min-w-0 pr-16
190
+ text-sm transition-colors duration-100
191
+ hover:bg-muted
192
+ ${isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'}
193
+ `}
194
+ >
195
+ {open
196
+ ? <FolderOpen size={14} className="text-yellow-400 shrink-0" />
197
+ : <Folder size={14} className="text-yellow-400 shrink-0" />
198
+ }
199
+ <span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
200
+ </button>
201
+ <div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/dir:flex items-center gap-0.5 z-10">
202
+ <button
203
+ type="button"
204
+ onClick={(e) => {
205
+ e.preventDefault();
206
+ e.stopPropagation();
207
+ setOpen(true);
208
+ setShowNewFile(true);
209
+ }}
210
+ className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
211
+ title={t.fileTree.newFileTitle}
212
+ >
213
+ <Plus size={13} />
214
+ </button>
215
+ <button
216
+ type="button"
217
+ onClick={startRename}
218
+ className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
219
+ title={t.fileTree.rename}
220
+ >
221
+ <Pencil size={12} />
222
+ </button>
223
+ </div>
224
+ </div>
225
+
226
+ <div
227
+ className="overflow-hidden transition-all duration-200"
228
+ style={{ maxHeight: open ? '9999px' : '0px' }}
229
+ >
230
+ {node.children && (
231
+ <FileTree nodes={node.children} depth={depth + 1} onNavigate={onNavigate} />
232
+ )}
233
+ {showNewFile && (
234
+ <NewFileInline
235
+ dirPath={node.path}
236
+ depth={depth}
237
+ onDone={() => setShowNewFile(false)}
238
+ />
239
+ )}
240
+ </div>
241
+ </div>
242
+ );
243
+ }
244
+
245
+ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
246
+ node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
247
+ }) {
248
+ const router = useRouter();
249
+ const isActive = currentPath === node.path;
250
+ const [renaming, setRenaming] = useState(false);
251
+ const [renameValue, setRenameValue] = useState(node.name);
252
+ const [isPending, startTransition] = useTransition();
253
+ const renameRef = useRef<HTMLInputElement>(null);
254
+ const { t } = useLocale();
255
+
256
+ const handleClick = useCallback(() => {
257
+ if (renaming) return;
258
+ router.push(`/view/${encodePath(node.path)}`);
259
+ onNavigate?.();
260
+ }, [router, node.path, onNavigate, renaming]);
261
+
262
+ const startRename = useCallback((e: React.MouseEvent) => {
263
+ e.stopPropagation();
264
+ setRenameValue(node.name);
265
+ setRenaming(true);
266
+ setTimeout(() => renameRef.current?.select(), 0);
267
+ }, [node.name]);
268
+
269
+ const commitRename = useCallback(() => {
270
+ const newName = renameValue.trim();
271
+ if (!newName || newName === node.name) { setRenaming(false); return; }
272
+ startTransition(async () => {
273
+ const result = await renameFileAction(node.path, newName);
274
+ if (result.success && result.newPath) {
275
+ setRenaming(false);
276
+ router.push(`/view/${encodePath(result.newPath)}`);
277
+ router.refresh();
278
+ } else {
279
+ setRenaming(false);
280
+ }
281
+ });
282
+ }, [renameValue, node.name, node.path, router]);
283
+
284
+ const handleDelete = useCallback((e: React.MouseEvent) => {
285
+ e.stopPropagation();
286
+ if (!confirm(t.fileTree.confirmDelete(node.name))) return;
287
+ startTransition(async () => {
288
+ await deleteFileAction(node.path);
289
+ if (currentPath === node.path) router.push('/');
290
+ router.refresh();
291
+ });
292
+ }, [node.name, node.path, currentPath, router, t]);
293
+
294
+ if (renaming) {
295
+ return (
296
+ <div className="relative px-2 py-0.5" style={{ paddingLeft: `${depth * 12 + 8}px` }}>
297
+ <input
298
+ ref={renameRef}
299
+ autoFocus
300
+ value={renameValue}
301
+ onChange={e => setRenameValue(e.target.value)}
302
+ onKeyDown={e => {
303
+ if (e.key === 'Enter') commitRename();
304
+ if (e.key === 'Escape') setRenaming(false);
305
+ }}
306
+ onBlur={commitRename}
307
+ className="w-full bg-muted border border-blue-500/60 rounded px-2 py-0.5 text-xs text-foreground focus:outline-none"
308
+ />
309
+ {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
310
+ </div>
311
+ );
312
+ }
313
+
314
+ return (
315
+ <div className="relative group/file">
316
+ <button
317
+ onClick={handleClick}
318
+ onDoubleClick={startRename}
319
+ data-filepath={node.path}
320
+ className={`
321
+ w-full flex items-center gap-1.5 px-2 py-1 rounded text-left
322
+ text-sm transition-colors duration-100 cursor-pointer pr-16
323
+ ${isActive
324
+ ? 'bg-accent text-foreground'
325
+ : 'hover:bg-muted text-muted-foreground hover:text-foreground'
326
+ }
327
+ `}
328
+ style={{ paddingLeft: `${depth * 12 + 8}px` }}
329
+ >
330
+ {getIcon(node)}
331
+ <span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
332
+ </button>
333
+ <div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
334
+ <button onClick={startRename} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.rename}>
335
+ <Pencil size={12} />
336
+ </button>
337
+ <button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-red-400 hover:bg-muted transition-colors" title={t.fileTree.delete}>
338
+ <Trash2 size={12} />
339
+ </button>
340
+ </div>
341
+ </div>
342
+ );
343
+ }
344
+
345
+ export default function FileTree({ nodes, depth = 0, onNavigate }: FileTreeProps) {
346
+ const pathname = usePathname();
347
+ const currentPath = getCurrentFilePath(pathname);
348
+
349
+ useEffect(() => {
350
+ if (!currentPath || depth !== 0) return;
351
+ const timer = setTimeout(() => {
352
+ const el = document.querySelector(`[data-filepath="${CSS.escape(currentPath)}"]`) as HTMLElement | null;
353
+ el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
354
+ }, 120);
355
+ return () => clearTimeout(timer);
356
+ }, [currentPath, depth]);
357
+
358
+ return (
359
+ <div className="flex flex-col gap-0.5">
360
+ {nodes.map((node) =>
361
+ node.type === 'directory' ? (
362
+ <DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} />
363
+ ) : (
364
+ <FileNodeItem key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} />
365
+ )
366
+ )}
367
+ </div>
368
+ );
369
+ }
@@ -0,0 +1,262 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+ import { encodePath, relativeTime } from '@/lib/utils';
8
+ import { getAllRenderers } from '@/lib/renderers/registry';
9
+ import '@/lib/renderers/index'; // registers all renderers
10
+
11
+ interface RecentFile {
12
+ path: string;
13
+ mtime: number;
14
+ }
15
+
16
+ // Maps a renderer id to a canonical entry file path
17
+ const RENDERER_ENTRY: Record<string, string> = {
18
+ todo: 'TODO.md',
19
+ csv: 'Resources/Products.csv',
20
+ graph: 'README.md',
21
+ timeline: 'CHANGELOG.md',
22
+ backlinks: 'BACKLINKS.md',
23
+ summary: 'DAILY.md',
24
+ 'agent-inspector': 'Agent-Audit.md',
25
+ workflow: 'Workflow.md',
26
+ 'diff-viewer': 'Agent-Diff.md',
27
+ 'config-panel': 'CONFIG.json',
28
+ };
29
+
30
+ function deriveEntryPath(id: string): string | null {
31
+ return RENDERER_ENTRY[id] ?? null;
32
+ }
33
+
34
+ function triggerSearch() {
35
+ // Dispatch ⌘K to open the Sidebar's SearchModal
36
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true }));
37
+ }
38
+
39
+ function triggerAsk() {
40
+ // Dispatch ⌘/ to open the Sidebar's AskModal
41
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: '/', metaKey: true, bubbles: true }));
42
+ }
43
+
44
+ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
45
+ const { t } = useLocale();
46
+ const [showAll, setShowAll] = useState(false);
47
+
48
+ const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
49
+
50
+ const renderers = getAllRenderers();
51
+
52
+ const lastFile = recent[0];
53
+
54
+ return (
55
+ <div className="content-width px-4 md:px-6 py-8 md:py-12">
56
+ {/* Hero */}
57
+ <div className="mb-10">
58
+ <div className="flex items-center gap-2 mb-3">
59
+ <div className="w-1 h-5 rounded-full" style={{ background: 'var(--amber)' }} />
60
+ <h1 className="text-2xl font-semibold tracking-tight" style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}>
61
+ MindOS
62
+ </h1>
63
+ </div>
64
+ <p className="text-sm leading-relaxed mb-5" style={{ color: 'var(--muted-foreground)', paddingLeft: '1rem' }}>
65
+ {t.app.tagline}
66
+ </p>
67
+
68
+ {/* AI-first command bar */}
69
+ <div
70
+ className="w-full max-w-[620px] flex flex-col sm:flex-row items-stretch sm:items-center gap-2"
71
+ style={{
72
+ marginLeft: '1rem',
73
+ }}
74
+ >
75
+ {/* Ask AI (primary) */}
76
+ <button
77
+ onClick={triggerAsk}
78
+ title="⌘/"
79
+ className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
80
+ style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
81
+ >
82
+ <Sparkles size={15} style={{ color: 'var(--amber)' }} className="shrink-0" />
83
+ <span className="text-sm flex-1 text-left" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
84
+ {t.home.shortcuts.askAI}
85
+ </span>
86
+ <kbd
87
+ className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-[11px] font-mono font-medium"
88
+ style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}
89
+ >
90
+ ⌘/
91
+ </kbd>
92
+ </button>
93
+
94
+ {/* Search files (secondary) */}
95
+ <button
96
+ onClick={triggerSearch}
97
+ title="⌘K"
98
+ className="flex items-center gap-2 px-3 py-3 rounded-xl border text-sm transition-colors shrink-0 hover:bg-muted"
99
+ style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
100
+ >
101
+ <Search size={14} />
102
+ <span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
103
+ <kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono" style={{ background: 'var(--muted)' }}>
104
+ ⌘K
105
+ </kbd>
106
+ </button>
107
+ </div>
108
+
109
+ {/* Quick Actions */}
110
+ <div className="flex flex-wrap gap-2.5 mt-4" style={{ paddingLeft: '1rem' }}>
111
+ {lastFile && (
112
+ <Link
113
+ href={`/view/${encodePath(lastFile.path)}`}
114
+ className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5"
115
+ style={{
116
+ background: 'var(--amber-dim)',
117
+ color: 'var(--amber)',
118
+ fontFamily: "'IBM Plex Sans', sans-serif",
119
+ }}
120
+ >
121
+ <ArrowRight size={14} />
122
+ <span>{t.home.continueEditing}</span>
123
+ <span className="text-xs opacity-60 truncate max-w-[160px]" suppressHydrationWarning>
124
+ {lastFile.path.split('/').pop()}
125
+ </span>
126
+ </Link>
127
+ )}
128
+ <Link
129
+ href="/view/Untitled.md"
130
+ className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors"
131
+ style={{
132
+ background: 'var(--muted)',
133
+ color: 'var(--muted-foreground)',
134
+ fontFamily: "'IBM Plex Sans', sans-serif",
135
+ }}
136
+ >
137
+ <FilePlus size={14} />
138
+ <span>{t.home.newNote}</span>
139
+ </Link>
140
+ </div>
141
+
142
+ </div>
143
+
144
+ {/* Plugins — compact 3-column grid */}
145
+ {renderers.length > 0 && (
146
+ <section className="mb-12">
147
+ <div className="flex items-center gap-2 mb-4">
148
+ <Puzzle size={13} style={{ color: 'var(--amber)' }} />
149
+ <h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
150
+ {t.home.plugins}
151
+ </h2>
152
+ <span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>
153
+ {renderers.length}
154
+ </span>
155
+ </div>
156
+
157
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
158
+ {renderers.map((r) => {
159
+ const entryPath = deriveEntryPath(r.id);
160
+ return (
161
+ <Link
162
+ key={r.id}
163
+ href={entryPath ? `/view/${encodePath(entryPath)}` : '#'}
164
+ className="group flex items-center gap-2.5 px-3 py-2.5 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
165
+ style={{ borderColor: 'var(--border)' }}
166
+ >
167
+ <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
168
+ <div className="flex-1 min-w-0">
169
+ <span className="text-xs font-semibold truncate block" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
170
+ {r.name}
171
+ </span>
172
+ </div>
173
+ </Link>
174
+ );
175
+ })}
176
+ </div>
177
+ </section>
178
+ )}
179
+
180
+ {/* Recently modified — timeline feed */}
181
+ {recent.length > 0 && (() => {
182
+ const INITIAL_COUNT = 5;
183
+ const visibleRecent = showAll ? recent : recent.slice(0, INITIAL_COUNT);
184
+ const hasMore = recent.length > INITIAL_COUNT;
185
+
186
+ return (
187
+ <section className="mb-12">
188
+ <div className="flex items-center gap-2 mb-5">
189
+ <Clock size={13} style={{ color: 'var(--amber)' }} />
190
+ <h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
191
+ {t.home.recentlyModified}
192
+ </h2>
193
+ </div>
194
+
195
+ <div className="relative pl-4">
196
+ {/* Timeline line */}
197
+ <div className="absolute left-0 top-1 bottom-1 w-px" style={{ background: 'var(--border)' }} />
198
+
199
+ <div className="flex flex-col gap-0.5">
200
+ {visibleRecent.map(({ path: filePath, mtime }, idx) => {
201
+ const isCSV = filePath.endsWith('.csv');
202
+ const name = filePath.split('/').pop() || filePath;
203
+ const dir = filePath.split('/').slice(0, -1).join('/');
204
+ return (
205
+ <div key={filePath} className="relative group">
206
+ {/* Timeline dot */}
207
+ <div
208
+ className="absolute -left-4 top-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full transition-all duration-150 group-hover:scale-150"
209
+ style={{
210
+ background: idx === 0 ? 'var(--amber)' : 'var(--border)',
211
+ outline: idx === 0 ? '2px solid var(--amber-dim)' : 'none',
212
+ }}
213
+ />
214
+ <Link
215
+ href={`/view/${encodePath(filePath)}`}
216
+ className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-100 group-hover:translate-x-0.5 hover:bg-muted"
217
+ >
218
+ {isCSV
219
+ ? <Table size={13} className="shrink-0" style={{ color: '#7aad80' }} />
220
+ : <FileText size={13} className="shrink-0" style={{ color: 'var(--muted-foreground)' }} />
221
+ }
222
+ <div className="flex-1 min-w-0">
223
+ <span className="text-sm font-medium truncate block" style={{ color: 'var(--foreground)' }} suppressHydrationWarning>{name}</span>
224
+ {dir && <span className="text-xs truncate block" style={{ color: 'var(--muted-foreground)', opacity: 0.6 }} suppressHydrationWarning>{dir}</span>}
225
+ </div>
226
+ <span className="text-xs shrink-0 tabular-nums" style={{ color: 'var(--muted-foreground)', opacity: 0.5, fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
227
+ {formatTime(mtime)}
228
+ </span>
229
+ </Link>
230
+ </div>
231
+ );
232
+ })}
233
+ </div>
234
+
235
+ {/* Show more / less */}
236
+ {hasMore && (
237
+ <button
238
+ onClick={() => setShowAll(v => !v)}
239
+ className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer"
240
+ style={{ color: 'var(--amber)', fontFamily: "'IBM Plex Mono', monospace" }}
241
+ >
242
+ <ChevronDown
243
+ size={12}
244
+ className="transition-transform duration-200"
245
+ style={{ transform: showAll ? 'rotate(180deg)' : undefined }}
246
+ />
247
+ <span>{showAll ? t.home.showLess : t.home.showMore}</span>
248
+ </button>
249
+ )}
250
+ </div>
251
+ </section>
252
+ );
253
+ })()}
254
+
255
+ {/* Footer */}
256
+ <div className="mt-16 flex items-center gap-1.5 text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.4, fontFamily: "'IBM Plex Mono', monospace" }}>
257
+ <Sparkles size={10} style={{ color: 'var(--amber)' }} />
258
+ <span>{t.app.footer}</span>
259
+ </div>
260
+ </div>
261
+ );
262
+ }
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ interface JsonViewProps {
6
+ content: string;
7
+ }
8
+
9
+ export default function JsonView({ content }: JsonViewProps) {
10
+ const pretty = useMemo(() => {
11
+ try {
12
+ return JSON.stringify(JSON.parse(content), null, 2);
13
+ } catch {
14
+ return content;
15
+ }
16
+ }, [content]);
17
+
18
+ return (
19
+ <pre
20
+ className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed"
21
+ style={{ fontFamily: "'IBM Plex Mono', monospace" }}
22
+ suppressHydrationWarning
23
+ >
24
+ <code>{pretty}</code>
25
+ </pre>
26
+ );
27
+ }