stellavault 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 (294) hide show
  1. package/.env.example +12 -0
  2. package/CLAUDE.md +39 -0
  3. package/CONTRIBUTING.md +65 -0
  4. package/LICENSE +21 -0
  5. package/README.md +182 -0
  6. package/memory/MEMORY.md +25 -0
  7. package/package.json +33 -0
  8. package/packages/cli/bin/ekh.js +2 -0
  9. package/packages/cli/bin/stellavault.js +2 -0
  10. package/packages/cli/dist/commands/brief-cmd.d.ts +2 -0
  11. package/packages/cli/dist/commands/brief-cmd.d.ts.map +1 -0
  12. package/packages/cli/dist/commands/brief-cmd.js +82 -0
  13. package/packages/cli/dist/commands/brief-cmd.js.map +1 -0
  14. package/packages/cli/dist/commands/capture-cmd.d.ts +7 -0
  15. package/packages/cli/dist/commands/capture-cmd.d.ts.map +1 -0
  16. package/packages/cli/dist/commands/capture-cmd.js +31 -0
  17. package/packages/cli/dist/commands/capture-cmd.js.map +1 -0
  18. package/packages/cli/dist/commands/card-cmd.d.ts +4 -0
  19. package/packages/cli/dist/commands/card-cmd.d.ts.map +1 -0
  20. package/packages/cli/dist/commands/card-cmd.js +26 -0
  21. package/packages/cli/dist/commands/card-cmd.js.map +1 -0
  22. package/packages/cli/dist/commands/clip-cmd.d.ts +4 -0
  23. package/packages/cli/dist/commands/clip-cmd.d.ts.map +1 -0
  24. package/packages/cli/dist/commands/clip-cmd.js +151 -0
  25. package/packages/cli/dist/commands/clip-cmd.js.map +1 -0
  26. package/packages/cli/dist/commands/cloud-cmd.d.ts +4 -0
  27. package/packages/cli/dist/commands/cloud-cmd.d.ts.map +1 -0
  28. package/packages/cli/dist/commands/cloud-cmd.js +64 -0
  29. package/packages/cli/dist/commands/cloud-cmd.js.map +1 -0
  30. package/packages/cli/dist/commands/contradictions-cmd.d.ts +2 -0
  31. package/packages/cli/dist/commands/contradictions-cmd.d.ts.map +1 -0
  32. package/packages/cli/dist/commands/contradictions-cmd.js +34 -0
  33. package/packages/cli/dist/commands/contradictions-cmd.js.map +1 -0
  34. package/packages/cli/dist/commands/decay-cmd.d.ts +2 -0
  35. package/packages/cli/dist/commands/decay-cmd.d.ts.map +1 -0
  36. package/packages/cli/dist/commands/decay-cmd.js +48 -0
  37. package/packages/cli/dist/commands/decay-cmd.js.map +1 -0
  38. package/packages/cli/dist/commands/digest-cmd.d.ts +4 -0
  39. package/packages/cli/dist/commands/digest-cmd.d.ts.map +1 -0
  40. package/packages/cli/dist/commands/digest-cmd.js +79 -0
  41. package/packages/cli/dist/commands/digest-cmd.js.map +1 -0
  42. package/packages/cli/dist/commands/duplicates-cmd.d.ts +4 -0
  43. package/packages/cli/dist/commands/duplicates-cmd.d.ts.map +1 -0
  44. package/packages/cli/dist/commands/duplicates-cmd.js +30 -0
  45. package/packages/cli/dist/commands/duplicates-cmd.js.map +1 -0
  46. package/packages/cli/dist/commands/federate-cmd.d.ts +5 -0
  47. package/packages/cli/dist/commands/federate-cmd.d.ts.map +1 -0
  48. package/packages/cli/dist/commands/federate-cmd.js +217 -0
  49. package/packages/cli/dist/commands/federate-cmd.js.map +1 -0
  50. package/packages/cli/dist/commands/gaps-cmd.d.ts +2 -0
  51. package/packages/cli/dist/commands/gaps-cmd.d.ts.map +1 -0
  52. package/packages/cli/dist/commands/gaps-cmd.js +33 -0
  53. package/packages/cli/dist/commands/gaps-cmd.js.map +1 -0
  54. package/packages/cli/dist/commands/graph-cmd.d.ts +2 -0
  55. package/packages/cli/dist/commands/graph-cmd.d.ts.map +1 -0
  56. package/packages/cli/dist/commands/graph-cmd.js +77 -0
  57. package/packages/cli/dist/commands/graph-cmd.js.map +1 -0
  58. package/packages/cli/dist/commands/index-cmd.d.ts +2 -0
  59. package/packages/cli/dist/commands/index-cmd.d.ts.map +1 -0
  60. package/packages/cli/dist/commands/index-cmd.js +57 -0
  61. package/packages/cli/dist/commands/index-cmd.js.map +1 -0
  62. package/packages/cli/dist/commands/init-cmd.d.ts +2 -0
  63. package/packages/cli/dist/commands/init-cmd.d.ts.map +1 -0
  64. package/packages/cli/dist/commands/init-cmd.js +123 -0
  65. package/packages/cli/dist/commands/init-cmd.js.map +1 -0
  66. package/packages/cli/dist/commands/learn-cmd.d.ts +2 -0
  67. package/packages/cli/dist/commands/learn-cmd.d.ts.map +1 -0
  68. package/packages/cli/dist/commands/learn-cmd.js +48 -0
  69. package/packages/cli/dist/commands/learn-cmd.js.map +1 -0
  70. package/packages/cli/dist/commands/pack-cmd.d.ts +15 -0
  71. package/packages/cli/dist/commands/pack-cmd.d.ts.map +1 -0
  72. package/packages/cli/dist/commands/pack-cmd.js +93 -0
  73. package/packages/cli/dist/commands/pack-cmd.js.map +1 -0
  74. package/packages/cli/dist/commands/review-cmd.d.ts +4 -0
  75. package/packages/cli/dist/commands/review-cmd.d.ts.map +1 -0
  76. package/packages/cli/dist/commands/review-cmd.js +107 -0
  77. package/packages/cli/dist/commands/review-cmd.js.map +1 -0
  78. package/packages/cli/dist/commands/search-cmd.d.ts +4 -0
  79. package/packages/cli/dist/commands/search-cmd.d.ts.map +1 -0
  80. package/packages/cli/dist/commands/search-cmd.js +38 -0
  81. package/packages/cli/dist/commands/search-cmd.js.map +1 -0
  82. package/packages/cli/dist/commands/serve-cmd.d.ts +2 -0
  83. package/packages/cli/dist/commands/serve-cmd.d.ts.map +1 -0
  84. package/packages/cli/dist/commands/serve-cmd.js +14 -0
  85. package/packages/cli/dist/commands/serve-cmd.js.map +1 -0
  86. package/packages/cli/dist/commands/status-cmd.d.ts +2 -0
  87. package/packages/cli/dist/commands/status-cmd.d.ts.map +1 -0
  88. package/packages/cli/dist/commands/status-cmd.js +33 -0
  89. package/packages/cli/dist/commands/status-cmd.js.map +1 -0
  90. package/packages/cli/dist/commands/sync-cmd.d.ts +5 -0
  91. package/packages/cli/dist/commands/sync-cmd.d.ts.map +1 -0
  92. package/packages/cli/dist/commands/sync-cmd.js +62 -0
  93. package/packages/cli/dist/commands/sync-cmd.js.map +1 -0
  94. package/packages/cli/dist/commands/vault-cmd.d.ts +10 -0
  95. package/packages/cli/dist/commands/vault-cmd.d.ts.map +1 -0
  96. package/packages/cli/dist/commands/vault-cmd.js +54 -0
  97. package/packages/cli/dist/commands/vault-cmd.js.map +1 -0
  98. package/packages/cli/dist/index.d.ts +2 -0
  99. package/packages/cli/dist/index.d.ts.map +1 -0
  100. package/packages/cli/dist/index.js +156 -0
  101. package/packages/cli/dist/index.js.map +1 -0
  102. package/packages/cli/package.json +24 -0
  103. package/packages/cli/src/commands/brief-cmd.ts +87 -0
  104. package/packages/cli/src/commands/capture-cmd.ts +34 -0
  105. package/packages/cli/src/commands/card-cmd.ts +29 -0
  106. package/packages/cli/src/commands/clip-cmd.ts +172 -0
  107. package/packages/cli/src/commands/cloud-cmd.ts +75 -0
  108. package/packages/cli/src/commands/contradictions-cmd.ts +41 -0
  109. package/packages/cli/src/commands/decay-cmd.ts +57 -0
  110. package/packages/cli/src/commands/digest-cmd.ts +89 -0
  111. package/packages/cli/src/commands/duplicates-cmd.ts +38 -0
  112. package/packages/cli/src/commands/federate-cmd.ts +236 -0
  113. package/packages/cli/src/commands/gaps-cmd.ts +40 -0
  114. package/packages/cli/src/commands/graph-cmd.ts +88 -0
  115. package/packages/cli/src/commands/index-cmd.ts +65 -0
  116. package/packages/cli/src/commands/init-cmd.ts +145 -0
  117. package/packages/cli/src/commands/learn-cmd.ts +56 -0
  118. package/packages/cli/src/commands/pack-cmd.ts +121 -0
  119. package/packages/cli/src/commands/review-cmd.ts +125 -0
  120. package/packages/cli/src/commands/search-cmd.ts +45 -0
  121. package/packages/cli/src/commands/serve-cmd.ts +17 -0
  122. package/packages/cli/src/commands/status-cmd.ts +37 -0
  123. package/packages/cli/src/commands/sync-cmd.ts +68 -0
  124. package/packages/cli/src/commands/vault-cmd.ts +64 -0
  125. package/packages/cli/src/index.ts +187 -0
  126. package/packages/core/package.json +40 -0
  127. package/packages/core/src/api/dashboard.ts +138 -0
  128. package/packages/core/src/api/graph-data.ts +286 -0
  129. package/packages/core/src/api/pwa.ts +82 -0
  130. package/packages/core/src/api/server.ts +660 -0
  131. package/packages/core/src/capture/voice.ts +168 -0
  132. package/packages/core/src/cloud/index.ts +2 -0
  133. package/packages/core/src/cloud/sync.ts +167 -0
  134. package/packages/core/src/config.ts +82 -0
  135. package/packages/core/src/federation/credits.ts +80 -0
  136. package/packages/core/src/federation/hyperswarm.d.ts +19 -0
  137. package/packages/core/src/federation/identity.ts +90 -0
  138. package/packages/core/src/federation/index.ts +8 -0
  139. package/packages/core/src/federation/node.ts +235 -0
  140. package/packages/core/src/federation/privacy.ts +52 -0
  141. package/packages/core/src/federation/reputation.ts +202 -0
  142. package/packages/core/src/federation/search.ts +129 -0
  143. package/packages/core/src/federation/sharing.ts +165 -0
  144. package/packages/core/src/federation/trust.ts +76 -0
  145. package/packages/core/src/federation/types.ts +25 -0
  146. package/packages/core/src/i18n/index.ts +85 -0
  147. package/packages/core/src/index.ts +133 -0
  148. package/packages/core/src/indexer/chunker.ts +180 -0
  149. package/packages/core/src/indexer/embedder.ts +9 -0
  150. package/packages/core/src/indexer/index.ts +113 -0
  151. package/packages/core/src/indexer/local-embedder.ts +35 -0
  152. package/packages/core/src/indexer/scanner.ts +142 -0
  153. package/packages/core/src/indexer/watcher.ts +62 -0
  154. package/packages/core/src/intelligence/contradiction-detector.ts +134 -0
  155. package/packages/core/src/intelligence/decay-engine.ts +229 -0
  156. package/packages/core/src/intelligence/duplicate-detector.ts +71 -0
  157. package/packages/core/src/intelligence/fsrs.ts +79 -0
  158. package/packages/core/src/intelligence/gap-detector.ts +109 -0
  159. package/packages/core/src/intelligence/learning-path.ts +86 -0
  160. package/packages/core/src/intelligence/notifications.ts +106 -0
  161. package/packages/core/src/intelligence/predictive-gaps.ts +94 -0
  162. package/packages/core/src/intelligence/semantic-versioning.ts +97 -0
  163. package/packages/core/src/intelligence/types.ts +28 -0
  164. package/packages/core/src/mcp/custom-tools.ts +97 -0
  165. package/packages/core/src/mcp/index.ts +1 -0
  166. package/packages/core/src/mcp/server.ts +142 -0
  167. package/packages/core/src/mcp/tools/agentic-graph.ts +96 -0
  168. package/packages/core/src/mcp/tools/brief.ts +49 -0
  169. package/packages/core/src/mcp/tools/decay.ts +40 -0
  170. package/packages/core/src/mcp/tools/decision-journal.ts +95 -0
  171. package/packages/core/src/mcp/tools/export.ts +72 -0
  172. package/packages/core/src/mcp/tools/federated-search.ts +43 -0
  173. package/packages/core/src/mcp/tools/generate-claude-md.ts +130 -0
  174. package/packages/core/src/mcp/tools/get-document.ts +26 -0
  175. package/packages/core/src/mcp/tools/get-related.ts +41 -0
  176. package/packages/core/src/mcp/tools/learning-path.ts +52 -0
  177. package/packages/core/src/mcp/tools/list-topics.ts +20 -0
  178. package/packages/core/src/mcp/tools/search.ts +35 -0
  179. package/packages/core/src/mcp/tools/snapshot.ts +98 -0
  180. package/packages/core/src/multi-vault/index.ts +118 -0
  181. package/packages/core/src/pack/creator.ts +127 -0
  182. package/packages/core/src/pack/exporter.ts +21 -0
  183. package/packages/core/src/pack/importer.ts +82 -0
  184. package/packages/core/src/pack/index.ts +5 -0
  185. package/packages/core/src/pack/marketplace.ts +103 -0
  186. package/packages/core/src/pack/pii-masker.ts +38 -0
  187. package/packages/core/src/pack/types.ts +39 -0
  188. package/packages/core/src/plugins/index.ts +100 -0
  189. package/packages/core/src/plugins/webhooks.ts +110 -0
  190. package/packages/core/src/search/bm25.ts +16 -0
  191. package/packages/core/src/search/index.ts +83 -0
  192. package/packages/core/src/search/rrf.ts +31 -0
  193. package/packages/core/src/search/semantic.ts +15 -0
  194. package/packages/core/src/store/index.ts +2 -0
  195. package/packages/core/src/store/sqlite-vec.ts +290 -0
  196. package/packages/core/src/store/types.ts +22 -0
  197. package/packages/core/src/team/index.ts +126 -0
  198. package/packages/core/src/types/chunk.ts +25 -0
  199. package/packages/core/src/types/document.ts +24 -0
  200. package/packages/core/src/types/graph.ts +44 -0
  201. package/packages/core/src/types/index.ts +15 -0
  202. package/packages/core/src/types/search.ts +38 -0
  203. package/packages/core/src/utils/retry.ts +85 -0
  204. package/packages/core/tests/api-card.test.ts +60 -0
  205. package/packages/core/tests/api-routes.test.ts +98 -0
  206. package/packages/core/tests/bm25.test.ts +87 -0
  207. package/packages/core/tests/chunker.test.ts +48 -0
  208. package/packages/core/tests/cluster.test.ts +75 -0
  209. package/packages/core/tests/constellation.test.ts +77 -0
  210. package/packages/core/tests/export-utils.test.ts +97 -0
  211. package/packages/core/tests/fsrs.test.ts +96 -0
  212. package/packages/core/tests/gesture-detector.test.ts +45 -0
  213. package/packages/core/tests/graph-data.test.ts +87 -0
  214. package/packages/core/tests/layout.test.ts +83 -0
  215. package/packages/core/tests/mcp.test.ts +148 -0
  216. package/packages/core/tests/pack.test.ts +127 -0
  217. package/packages/core/tests/pii-masker.test.ts +42 -0
  218. package/packages/core/tests/profile-card.test.ts +62 -0
  219. package/packages/core/tests/rrf.test.ts +29 -0
  220. package/packages/core/tests/search-integration.test.ts +139 -0
  221. package/packages/core/tests/store.test.ts +80 -0
  222. package/packages/graph/click-result.png +0 -0
  223. package/packages/graph/index.html +17 -0
  224. package/packages/graph/package.json +32 -0
  225. package/packages/graph/src/App.tsx +7 -0
  226. package/packages/graph/src/api/client.ts +39 -0
  227. package/packages/graph/src/components/ClusterFilter.tsx +73 -0
  228. package/packages/graph/src/components/ConstellationView.tsx +232 -0
  229. package/packages/graph/src/components/ExportPanel.tsx +177 -0
  230. package/packages/graph/src/components/Graph3D.tsx +230 -0
  231. package/packages/graph/src/components/GraphEdges.tsx +100 -0
  232. package/packages/graph/src/components/GraphNodes.tsx +386 -0
  233. package/packages/graph/src/components/HealthDashboard.tsx +173 -0
  234. package/packages/graph/src/components/Layout.tsx +214 -0
  235. package/packages/graph/src/components/MotionOverlay.tsx +81 -0
  236. package/packages/graph/src/components/MotionToggle.tsx +33 -0
  237. package/packages/graph/src/components/MultiverseView.tsx +286 -0
  238. package/packages/graph/src/components/NodeDetail.tsx +232 -0
  239. package/packages/graph/src/components/PulseParticle.tsx +232 -0
  240. package/packages/graph/src/components/SearchBar.tsx +107 -0
  241. package/packages/graph/src/components/StarField.tsx +197 -0
  242. package/packages/graph/src/components/StatusBar.tsx +53 -0
  243. package/packages/graph/src/components/Timeline.tsx +148 -0
  244. package/packages/graph/src/components/ToolsPanel.tsx +512 -0
  245. package/packages/graph/src/components/Tooltip.tsx +100 -0
  246. package/packages/graph/src/components/TypeFilter.tsx +131 -0
  247. package/packages/graph/src/embed/EmbedGraph.tsx +144 -0
  248. package/packages/graph/src/hooks/useConstellationLOD.ts +76 -0
  249. package/packages/graph/src/hooks/useDecay.ts +37 -0
  250. package/packages/graph/src/hooks/useExport.ts +165 -0
  251. package/packages/graph/src/hooks/useGraph.ts +69 -0
  252. package/packages/graph/src/hooks/useKeyboardNav.ts +122 -0
  253. package/packages/graph/src/hooks/useLayout.ts +45 -0
  254. package/packages/graph/src/hooks/useMotion.ts +120 -0
  255. package/packages/graph/src/hooks/usePulse.ts +58 -0
  256. package/packages/graph/src/hooks/useSearch.ts +71 -0
  257. package/packages/graph/src/lib/constellation.ts +107 -0
  258. package/packages/graph/src/lib/export-utils.ts +48 -0
  259. package/packages/graph/src/lib/gesture-detector.ts +123 -0
  260. package/packages/graph/src/lib/layout.worker.ts +153 -0
  261. package/packages/graph/src/lib/motion-controller.ts +83 -0
  262. package/packages/graph/src/lib/profile-card.ts +122 -0
  263. package/packages/graph/src/main.tsx +4 -0
  264. package/packages/graph/src/stores/graph-store.ts +155 -0
  265. package/packages/graph/success.png +0 -0
  266. package/packages/graph/test-click.mjs +49 -0
  267. package/packages/graph/test-explore.mjs +102 -0
  268. package/packages/graph/test-final.mjs +61 -0
  269. package/packages/graph/test-graph.mjs +139 -0
  270. package/packages/graph/test-hover.mjs +48 -0
  271. package/packages/graph/test-pulse.mjs +68 -0
  272. package/packages/graph/test-screenshot.mjs +56 -0
  273. package/packages/graph/test-v2.mjs +97 -0
  274. package/packages/graph/vite.config.ts +15 -0
  275. package/packages/sync/.env.example +11 -0
  276. package/packages/sync/.sync-state.json +317 -0
  277. package/packages/sync/.upload-state.json +1009 -0
  278. package/packages/sync/create-stella-network-notion.mjs +151 -0
  279. package/packages/sync/create-stellavault-project-notion.mjs +322 -0
  280. package/packages/sync/logs/sync-2026-03-28.log +6 -0
  281. package/packages/sync/logs/sync-2026-03-29.log +12 -0
  282. package/packages/sync/logs/sync-2026-03-30.log +6 -0
  283. package/packages/sync/logs/sync-2026-03-31.log +6 -0
  284. package/packages/sync/logs/sync-2026-04-01.log +6 -0
  285. package/packages/sync/logs/sync-2026-04-02.log +6 -0
  286. package/packages/sync/package-lock.json +373 -0
  287. package/packages/sync/package.json +16 -0
  288. package/packages/sync/run-sync.bat +18 -0
  289. package/packages/sync/run-sync.mjs +46 -0
  290. package/packages/sync/setup-scheduler.mjs +119 -0
  291. package/packages/sync/structured-sync.mjs +187 -0
  292. package/packages/sync/sync-to-obsidian.mjs +264 -0
  293. package/packages/sync/upload-pdca-to-notion.mjs +495 -0
  294. package/tsconfig.base.json +18 -0
@@ -0,0 +1,386 @@
1
+ // 노드 렌더링: Points (포인트 클라우드) — 확실한 가시성 + 최고 성능
2
+ // 옵시디언 스타일: 호버 시 연결 노드 강조, 나머지 페이드
3
+
4
+ import { useRef, useMemo, useEffect, useCallback } from 'react';
5
+ import { useFrame } from '@react-three/fiber';
6
+ import * as THREE from 'three';
7
+ import { useGraphStore } from '../stores/graph-store.js';
8
+
9
+ // 원형 포인트 텍스처 생성
10
+ function createCircleTexture(): THREE.Texture {
11
+ const size = 64;
12
+ const canvas = document.createElement('canvas');
13
+ canvas.width = size;
14
+ canvas.height = size;
15
+ const ctx = canvas.getContext('2d')!;
16
+
17
+ // 그라디언트 원 (가운데 밝고 가장자리 페이드)
18
+ const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
19
+ gradient.addColorStop(0, 'rgba(255,255,255,1)');
20
+ gradient.addColorStop(0.3, 'rgba(255,255,255,0.8)');
21
+ gradient.addColorStop(0.7, 'rgba(255,255,255,0.3)');
22
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
23
+ ctx.fillStyle = gradient;
24
+ ctx.fillRect(0, 0, size, size);
25
+
26
+ const tex = new THREE.CanvasTexture(canvas);
27
+ tex.needsUpdate = true;
28
+ return tex;
29
+ }
30
+
31
+ const circleTexture = createCircleTexture();
32
+
33
+ // 선명한 15색 팔레트
34
+ const PALETTE = [
35
+ [0.49, 0.23, 0.93], // #7c3aed 보라
36
+ [0.93, 0.27, 0.60], // #ec4899 핑크
37
+ [0.96, 0.62, 0.04], // #f59e0b 노랑
38
+ [0.06, 0.72, 0.51], // #10b981 초록
39
+ [0.23, 0.51, 0.96], // #3b82f6 파랑
40
+ [0.94, 0.27, 0.27], // #ef4444 빨강
41
+ [0.02, 0.71, 0.83], // #06b6d4 시안
42
+ [0.52, 0.80, 0.09], // #84cc16 라임
43
+ [0.98, 0.57, 0.09], // #f97316 오렌지
44
+ [0.55, 0.36, 0.96], // #8b5cf6 인디고
45
+ [0.08, 0.72, 0.65], // #14b8a6 틸
46
+ [0.91, 0.47, 0.98], // #e879f9 퓨시아
47
+ [0.92, 0.80, 0.03], // #eab308 골드
48
+ [0.13, 0.83, 0.93], // #22d3ee 스카이
49
+ [0.98, 0.45, 0.52], // #fb7185 코랄
50
+ ] as number[][];
51
+
52
+ export function GraphNodes() {
53
+ const pointsRef = useRef<THREE.Points>(null);
54
+ const glowRef = useRef<THREE.Points>(null);
55
+
56
+ const nodes = useGraphStore((s) => s.nodes);
57
+ const edges = useGraphStore((s) => s.edges);
58
+ const selectedNodeId = useGraphStore((s) => s.selectedNodeId);
59
+ const hoveredNodeId = useGraphStore((s) => s.hoveredNodeId);
60
+ const highlightedNodeIds = useGraphStore((s) => s.highlightedNodeIds);
61
+ const selectNode = useGraphStore((s) => s.selectNode);
62
+ const hoverNode = useGraphStore((s) => s.hoverNode);
63
+ const hiddenClusters = useGraphStore((s) => s.hiddenClusters);
64
+ const hiddenTypes = useGraphStore((s) => s.hiddenTypes);
65
+ const timelineRange = useGraphStore((s) => s.timelineRange);
66
+ const theme = useGraphStore((s) => s.theme);
67
+ const isLight = theme === 'light';
68
+ const lodLevel = useGraphStore((s) => s.lodLevel);
69
+ const showDecayOverlay = useGraphStore((s) => s.showDecayOverlay);
70
+ const decayData = useGraphStore((s) => s.decayData);
71
+
72
+ // LOD nodeScale 적용 — Design Ref: §8
73
+ const lodScale = lodLevel === 'universe' ? 0.6 : lodLevel === 'note' ? 1.2 : 1.0;
74
+
75
+ // 검색 결과 breathing pulse 애니메이션
76
+ const pulseTimeRef = useRef(0);
77
+
78
+ useFrame(() => {
79
+ if (highlightedNodeIds.size === 0) { pulseTimeRef.current = 0; return; }
80
+
81
+ const pts = pointsRef.current;
82
+ if (!pts || nodes.length === 0) return;
83
+ const sizeAttr = pts.geometry.getAttribute('size') as THREE.BufferAttribute;
84
+ if (!sizeAttr) return;
85
+
86
+ pulseTimeRef.current += 0.04;
87
+ // sin wave breathing: 1.4x ~ 2.2x 반복
88
+ const breath = 1.4 + Math.sin(pulseTimeRef.current) * 0.4;
89
+
90
+ for (let i = 0; i < nodes.length; i++) {
91
+ if (highlightedNodeIds.has(nodes[i].id)) {
92
+ const baseSize = 4 + nodes[i].size * 3;
93
+ sizeAttr.setX(i, baseSize * breath);
94
+ }
95
+ }
96
+ sizeAttr.needsUpdate = true;
97
+ });
98
+
99
+ // 호버/선택 시 이웃 노드
100
+ const connectedIds = useMemo(() => {
101
+ const activeId = hoveredNodeId || selectedNodeId;
102
+ if (!activeId) return null;
103
+ const ids = new Set<string>();
104
+ ids.add(activeId);
105
+ for (const e of edges) {
106
+ if (e.source === activeId) ids.add(e.target);
107
+ if (e.target === activeId) ids.add(e.source);
108
+ }
109
+ return ids;
110
+ }, [hoveredNodeId, selectedNodeId, edges]);
111
+
112
+ // 위치 + 색상 + 크기 버퍼
113
+ const { positions, colors, sizes, glowSizes } = useMemo(() => {
114
+ const n = nodes.length;
115
+ const pos = new Float32Array(n * 3);
116
+ const col = new Float32Array(n * 3);
117
+ const sz = new Float32Array(n);
118
+ const gsz = new Float32Array(n);
119
+
120
+ for (let i = 0; i < n; i++) {
121
+ const node = nodes[i];
122
+ const [x, y, z] = node.position ?? [0, 0, 0];
123
+ pos[i * 3] = x;
124
+ pos[i * 3 + 1] = y;
125
+ pos[i * 3 + 2] = z;
126
+
127
+ const pal = PALETTE[node.clusterId % PALETTE.length];
128
+ if (isLight) {
129
+ // Light mode: 모노톤 — 크기 무관하게 일관된 진한 회색 (모든 노드 가시성 확보)
130
+ const gray = 0.32 + (1 - Math.min(node.size / 7, 1)) * 0.12;
131
+ col[i * 3] = gray;
132
+ col[i * 3 + 1] = gray;
133
+ col[i * 3 + 2] = gray + 0.02;
134
+ } else {
135
+ // Dark mode: 기존 팔레트 + 밝기 부스트
136
+ const bright = Math.min((node.size - 1) / 6, 1) * 0.4;
137
+ col[i * 3] = Math.min(pal[0] + bright, 1);
138
+ col[i * 3 + 1] = Math.min(pal[1] + bright, 1);
139
+ col[i * 3 + 2] = Math.min(pal[2] + bright, 1);
140
+ }
141
+
142
+ sz[i] = (3 + node.size * 4) * lodScale;
143
+ gsz[i] = (8 + node.size * 12) * lodScale;
144
+ }
145
+
146
+ return { positions: pos, colors: col, sizes: sz, glowSizes: gsz };
147
+ }, [nodes, isLight, lodScale]);
148
+
149
+ // 호버/선택 시 컬러 + 크기 업데이트 (극적 효과)
150
+ useEffect(() => {
151
+ const pts = pointsRef.current;
152
+ const glow = glowRef.current;
153
+ if (!pts || nodes.length === 0) return;
154
+
155
+ const colAttr = pts.geometry.getAttribute('color') as THREE.BufferAttribute;
156
+ const sizeAttr = pts.geometry.getAttribute('size') as THREE.BufferAttribute;
157
+ const glowColAttr = glow?.geometry.getAttribute('color') as THREE.BufferAttribute | undefined;
158
+ const glowSizeAttr = glow?.geometry.getAttribute('size') as THREE.BufferAttribute | undefined;
159
+ if (!colAttr || !sizeAttr) return;
160
+
161
+ const hasPulse = highlightedNodeIds.size > 0;
162
+ const hasActive = connectedIds !== null && !hasPulse;
163
+ const currentTheme = useGraphStore.getState().theme;
164
+ const isLightMode = currentTheme === 'light';
165
+ // hiddenClusters는 컴포넌트 레벨에서 subscribe
166
+
167
+ for (let i = 0; i < nodes.length; i++) {
168
+ const node = nodes[i];
169
+ const pal = PALETTE[node.clusterId % PALETTE.length];
170
+ let r = pal[0], g = pal[1], b = pal[2];
171
+ let sz = 4 + node.size * 3;
172
+ let gsz = 12 + node.size * 8;
173
+
174
+ // 감쇠 오버레이 — Design Ref: §5.1
175
+ if (showDecayOverlay && !hasPulse && !hasActive) {
176
+ const rVal = decayData[node.id] ?? 1.0; // R값 (없으면 1.0 = 건강)
177
+ if (rVal < 0.7) {
178
+ const fade = Math.max(rVal, 0.1);
179
+ if (isLightMode) {
180
+ r = 0.5 + (1 - fade) * 0.4; g = 0.5 + (1 - fade) * 0.4; b = 0.5 + (1 - fade) * 0.4;
181
+ } else {
182
+ r *= fade; g *= fade; b *= fade;
183
+ }
184
+ sz *= (0.3 + fade * 0.7);
185
+ gsz *= (0.2 + fade * 0.5);
186
+ }
187
+ colAttr.setXYZ(i, r, g, b);
188
+ sizeAttr.setX(i, sz);
189
+ if (glowColAttr) glowColAttr.setXYZ(i, r, g, b);
190
+ if (glowSizeAttr) glowSizeAttr.setX(i, gsz);
191
+ continue;
192
+ }
193
+
194
+ // 타임라인 범위 필터
195
+ if (timelineRange && node.lastModified) {
196
+ const ms = new Date(node.lastModified).getTime();
197
+ if (ms < timelineRange[0] || ms > timelineRange[1]) {
198
+ r *= 0.04; g *= 0.04; b *= 0.04;
199
+ sz *= 0.2;
200
+ gsz *= 0.1;
201
+ colAttr.setXYZ(i, r, g, b);
202
+ sizeAttr.setX(i, sz);
203
+ if (glowColAttr) glowColAttr.setXYZ(i, r, g, b);
204
+ if (glowSizeAttr) glowSizeAttr.setX(i, gsz);
205
+ continue;
206
+ }
207
+ }
208
+
209
+ // type/source 숨김
210
+ const nodeSource = node.source ?? 'local';
211
+ const nodeType = node.type ?? 'note';
212
+ if (hiddenTypes.has(`source:${nodeSource}`) || hiddenTypes.has(`type:${nodeType}`)) {
213
+ r *= 0.02; g *= 0.02; b *= 0.02;
214
+ sz *= 0.15;
215
+ gsz *= 0.1;
216
+ colAttr.setXYZ(i, r, g, b);
217
+ sizeAttr.setX(i, sz);
218
+ if (glowColAttr) glowColAttr.setXYZ(i, r, g, b);
219
+ if (glowSizeAttr) glowSizeAttr.setX(i, gsz);
220
+ continue;
221
+ }
222
+
223
+ // 클러스터 숨김
224
+ if (hiddenClusters.has(node.clusterId)) {
225
+ r *= 0.02; g *= 0.02; b *= 0.02;
226
+ sz *= 0.15;
227
+ gsz *= 0.1;
228
+ colAttr.setXYZ(i, r, g, b);
229
+ sizeAttr.setX(i, sz);
230
+ if (glowColAttr) glowColAttr.setXYZ(i, r, g, b);
231
+ if (glowSizeAttr) glowSizeAttr.setX(i, gsz);
232
+ continue;
233
+ }
234
+
235
+ if (hasPulse) {
236
+ if (highlightedNodeIds.has(node.id)) {
237
+ if (isLightMode) {
238
+ // Light: 하이라이트 시 클러스터 컬러 드러남 (모노톤→컬러)
239
+ r = pal[0] * 0.7; g = pal[1] * 0.7; b = pal[2] * 0.7;
240
+ sz *= 1.8;
241
+ } else {
242
+ r = Math.min(r * 1.6, 1);
243
+ g = Math.min(g * 1.6, 1);
244
+ b = Math.min(b * 1.6, 1);
245
+ sz *= 1.6;
246
+ }
247
+ gsz = isLightMode ? 0 : gsz * 2;
248
+ } else {
249
+ if (isLightMode) {
250
+ r = 0.90; g = 0.90; b = 0.91;
251
+ sz *= 0.15;
252
+ } else {
253
+ r *= 0.03; g *= 0.03; b *= 0.03;
254
+ sz *= 0.3;
255
+ gsz *= 0.2;
256
+ }
257
+ gsz = isLightMode ? 0 : gsz;
258
+ }
259
+ } else if (node.id === hoveredNodeId) {
260
+ if (isLightMode) {
261
+ // Light: 호버 시 클러스터 컬러로 전환
262
+ r = pal[0] * 0.65; g = pal[1] * 0.65; b = pal[2] * 0.65;
263
+ } else {
264
+ r = 1; g = 1; b = 1;
265
+ }
266
+ sz *= 2.5;
267
+ gsz = isLightMode ? 0 : gsz * 2.5;
268
+ } else if (node.id === selectedNodeId) {
269
+ if (isLightMode) {
270
+ // Light: 선택 시 클러스터 컬러
271
+ r = pal[0] * 0.6 + 0.15;
272
+ g = pal[1] * 0.6 + 0.15;
273
+ b = pal[2] * 0.6 + 0.15;
274
+ } else {
275
+ r = r * 0.3 + 0.7;
276
+ g = g * 0.3 + 0.7;
277
+ b = b * 0.3 + 0.7;
278
+ }
279
+ sz *= 2;
280
+ gsz = isLightMode ? 0 : gsz * 2;
281
+ } else if (hasActive && connectedIds!.has(node.id)) {
282
+ if (isLightMode) {
283
+ // Light: 연결 노드도 컬러 드러남
284
+ r = pal[0] * 0.7; g = pal[1] * 0.7; b = pal[2] * 0.7;
285
+ } else {
286
+ r = Math.min(r * 1.6, 1);
287
+ g = Math.min(g * 1.6, 1);
288
+ b = Math.min(b * 1.6, 1);
289
+ }
290
+ sz *= 1.5;
291
+ gsz = isLightMode ? 0 : gsz * 1.8;
292
+ } else if (hasActive && !connectedIds!.has(node.id)) {
293
+ if (isLightMode) {
294
+ r = 0.90; g = 0.90; b = 0.91;
295
+ sz *= 0.15;
296
+ gsz = 0;
297
+ } else {
298
+ r *= 0.03; g *= 0.03; b *= 0.03;
299
+ sz *= 0.4;
300
+ gsz *= 0.3;
301
+ }
302
+ }
303
+
304
+ colAttr.setXYZ(i, r, g, b);
305
+ sizeAttr.setX(i, sz);
306
+ if (glowColAttr) glowColAttr.setXYZ(i, r, g, b);
307
+ if (glowSizeAttr) glowSizeAttr.setX(i, gsz);
308
+ }
309
+
310
+ colAttr.needsUpdate = true;
311
+ sizeAttr.needsUpdate = true;
312
+ if (glowColAttr) glowColAttr.needsUpdate = true;
313
+ if (glowSizeAttr) glowSizeAttr.needsUpdate = true;
314
+ }, [nodes, hoveredNodeId, selectedNodeId, connectedIds, highlightedNodeIds, hiddenClusters, hiddenTypes, timelineRange, showDecayOverlay, decayData]);
315
+
316
+ // Raycaster로 호버/클릭 처리
317
+ const handlePointerMove = useCallback((e: any) => {
318
+ e.stopPropagation();
319
+ if (e.index !== undefined && e.index < nodes.length) {
320
+ hoverNode(nodes[e.index].id);
321
+ document.body.style.cursor = 'pointer';
322
+ }
323
+ }, [nodes, hoverNode]);
324
+
325
+ const handleClick = useCallback((e: any) => {
326
+ e.stopPropagation();
327
+ if (e.index !== undefined && e.index < nodes.length) {
328
+ selectNode(nodes[e.index].id);
329
+ }
330
+ }, [nodes, selectNode]);
331
+
332
+ const handlePointerOut = useCallback(() => {
333
+ hoverNode(null);
334
+ document.body.style.cursor = 'default';
335
+ }, [hoverNode]);
336
+
337
+ if (nodes.length === 0) return null;
338
+
339
+ return (
340
+ <group>
341
+ {/* 글로우 레이어 (큰 반투명 포인트) — light mode에서는 그림자 스타일 */}
342
+ <points ref={glowRef}>
343
+ <bufferGeometry>
344
+ <bufferAttribute attach="attributes-position" args={[positions, 3]} />
345
+ <bufferAttribute attach="attributes-color" args={[new Float32Array(colors), 3]} />
346
+ <bufferAttribute attach="attributes-size" args={[glowSizes, 1]} />
347
+ </bufferGeometry>
348
+ <pointsMaterial
349
+ vertexColors
350
+ transparent
351
+ opacity={isLight ? 0.06 : 0.25}
352
+ depthWrite={false}
353
+ blending={isLight ? THREE.NormalBlending : THREE.AdditiveBlending}
354
+ sizeAttenuation
355
+ size={isLight ? 10 : 18}
356
+ map={circleTexture}
357
+ alphaTest={0.05}
358
+ />
359
+ </points>
360
+
361
+ {/* 코어 노드 */}
362
+ <points
363
+ ref={pointsRef}
364
+ onClick={handleClick}
365
+ onPointerOver={handlePointerMove}
366
+ onPointerOut={handlePointerOut}
367
+ >
368
+ <bufferGeometry>
369
+ <bufferAttribute attach="attributes-position" args={[positions, 3]} />
370
+ <bufferAttribute attach="attributes-color" args={[colors, 3]} />
371
+ <bufferAttribute attach="attributes-size" args={[sizes, 1]} />
372
+ </bufferGeometry>
373
+ <pointsMaterial
374
+ vertexColors
375
+ transparent
376
+ opacity={isLight ? 1.0 : 0.95}
377
+ depthWrite={false}
378
+ sizeAttenuation
379
+ size={isLight ? 8 : 6}
380
+ map={circleTexture}
381
+ alphaTest={0.01}
382
+ />
383
+ </points>
384
+ </group>
385
+ );
386
+ }
@@ -0,0 +1,173 @@
1
+ // 노트 건강도 대시보드 — decay/gaps/duplicates/growth 종합 뷰
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { fetchHealth } from '../api/client.js';
5
+ import { useGraphStore } from '../stores/graph-store.js';
6
+
7
+ interface HealthData {
8
+ stats: { documentCount: number; chunkCount: number; dbSizeMB?: number; vaultName?: string };
9
+ decay: { totalDocuments: number; criticalCount: number; decayingCount: number; averageR: number; topDecaying: Array<{ title: string; retrievability: number }> };
10
+ gaps: { gapCount: number; isolatedCount: number };
11
+ duplicates: { count: number };
12
+ distribution: { source: Record<string, number>; type: Record<string, number> };
13
+ growth: Record<string, number>;
14
+ }
15
+
16
+ function MetricCard({ label, value, sub, color }: { label: string; value: string | number; sub?: string; color: string }) {
17
+ const theme = useGraphStore((s) => s.theme);
18
+ const isDark = theme === 'dark';
19
+ return (
20
+ <div style={{
21
+ padding: '10px 12px', borderRadius: '8px', flex: '1 1 120px', minWidth: '120px',
22
+ background: isDark ? 'rgba(100,120,255,0.06)' : 'rgba(0,0,0,0.02)',
23
+ border: `1px solid ${isDark ? 'rgba(100,120,255,0.1)' : 'rgba(0,0,0,0.06)'}`,
24
+ }}>
25
+ <div style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px', color: isDark ? '#667' : '#999', marginBottom: '4px' }}>
26
+ {label}
27
+ </div>
28
+ <div style={{ fontSize: '20px', fontWeight: 700, color, lineHeight: 1.2 }}>
29
+ {value}
30
+ </div>
31
+ {sub && <div style={{ fontSize: '10px', color: isDark ? '#556' : '#888', marginTop: '2px' }}>{sub}</div>}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ function MiniBar({ items, isDark }: { items: Array<{ label: string; value: number; color: string }>; isDark: boolean }) {
37
+ const total = items.reduce((s, i) => s + i.value, 0) || 1;
38
+ return (
39
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
40
+ {items.map((item) => (
41
+ <div key={item.label} style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '10px' }}>
42
+ <span style={{ width: '50px', color: isDark ? '#aab' : '#555', textAlign: 'right' }}>{item.label}</span>
43
+ <div style={{ flex: 1, height: '6px', background: isDark ? 'rgba(100,120,255,0.08)' : 'rgba(0,0,0,0.04)', borderRadius: '3px', overflow: 'hidden' }}>
44
+ <div style={{ width: `${(item.value / total) * 100}%`, height: '100%', background: item.color, borderRadius: '3px' }} />
45
+ </div>
46
+ <span style={{ width: '30px', color: isDark ? '#667' : '#999', fontSize: '9px' }}>{item.value}</span>
47
+ </div>
48
+ ))}
49
+ </div>
50
+ );
51
+ }
52
+
53
+ const SOURCE_COLORS: Record<string, string> = {
54
+ local: '#10b981', notion: '#3b82f6', clip: '#f59e0b', bridge: '#8b5cf6', pack: '#ec4899',
55
+ };
56
+
57
+ const TYPE_COLORS: Record<string, string> = {
58
+ note: '#3b82f6', clip: '#f59e0b', sync: '#10b981', bridge: '#8b5cf6', decision: '#ef4444', snapshot: '#06b6d4',
59
+ };
60
+
61
+ export function HealthDashboard() {
62
+ const theme = useGraphStore((s) => s.theme);
63
+ const isDark = theme === 'dark';
64
+ const [data, setData] = useState<HealthData | null>(null);
65
+ const [loading, setLoading] = useState(false);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ useEffect(() => {
69
+ setLoading(true);
70
+ fetchHealth()
71
+ .then(setData)
72
+ .catch((err) => setError(String(err)))
73
+ .finally(() => setLoading(false));
74
+ }, []);
75
+
76
+ if (loading) return <div style={{ padding: '20px', color: isDark ? '#667' : '#999', fontSize: '12px' }}>Loading health data...</div>;
77
+ if (error) return <div style={{ padding: '20px', color: '#ef4444', fontSize: '12px' }}>{error}</div>;
78
+ if (!data) return null;
79
+
80
+ const rPct = Math.round(data.decay.averageR * 100);
81
+ const rColor = rPct >= 70 ? '#10b981' : rPct >= 40 ? '#f59e0b' : '#ef4444';
82
+
83
+ const sourceItems = Object.entries(data.distribution.source).map(([label, value]) => ({
84
+ label, value, color: SOURCE_COLORS[label] ?? '#666',
85
+ }));
86
+ const typeItems = Object.entries(data.distribution.type).map(([label, value]) => ({
87
+ label, value, color: TYPE_COLORS[label] ?? '#666',
88
+ }));
89
+
90
+ // 성장 추이 — 최근 12개월 미니 차트
91
+ const growthEntries = Object.entries(data.growth).slice(-12);
92
+ const maxGrowth = Math.max(1, ...growthEntries.map(([, v]) => v));
93
+
94
+ return (
95
+ <div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
96
+ {/* 핵심 메트릭 */}
97
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
98
+ <MetricCard label="Total Docs" value={data.stats.documentCount} sub={`${data.stats.chunkCount} chunks`} color={isDark ? '#c0c0f0' : '#2a2a4a'} />
99
+ <MetricCard label="Avg Retrievability" value={`${rPct}%`} sub={`${data.decay.criticalCount} critical`} color={rColor} />
100
+ <MetricCard label="Knowledge Gaps" value={data.gaps.gapCount} sub={`${data.gaps.isolatedCount} isolated`} color={data.gaps.gapCount > 5 ? '#f59e0b' : '#10b981'} />
101
+ <MetricCard label="Duplicates" value={data.duplicates.count} color={data.duplicates.count > 10 ? '#f59e0b' : '#10b981'} />
102
+ </div>
103
+
104
+ {/* 분포 */}
105
+ <div style={{ display: 'flex', gap: '16px' }}>
106
+ <div style={{ flex: 1 }}>
107
+ <div style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px', color: isDark ? '#667' : '#999', marginBottom: '6px' }}>
108
+ By Source
109
+ </div>
110
+ <MiniBar items={sourceItems} isDark={isDark} />
111
+ </div>
112
+ <div style={{ flex: 1 }}>
113
+ <div style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px', color: isDark ? '#667' : '#999', marginBottom: '6px' }}>
114
+ By Type
115
+ </div>
116
+ <MiniBar items={typeItems} isDark={isDark} />
117
+ </div>
118
+ </div>
119
+
120
+ {/* 성장 추이 */}
121
+ {growthEntries.length > 1 && (
122
+ <div>
123
+ <div style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px', color: isDark ? '#667' : '#999', marginBottom: '6px' }}>
124
+ Monthly Growth
125
+ </div>
126
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: '2px', height: '40px' }}>
127
+ {growthEntries.map(([month, count]) => (
128
+ <div key={month} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
129
+ <div style={{
130
+ width: '100%', maxWidth: '24px',
131
+ height: `${(count / maxGrowth) * 100}%`, minHeight: '2px',
132
+ background: isDark ? 'rgba(100,180,255,0.5)' : 'rgba(59,130,246,0.4)',
133
+ borderRadius: '2px 2px 0 0',
134
+ }} />
135
+ </div>
136
+ ))}
137
+ </div>
138
+ <div style={{ display: 'flex', gap: '2px', marginTop: '2px' }}>
139
+ {growthEntries.map(([month]) => (
140
+ <div key={month} style={{ flex: 1, fontSize: '7px', color: isDark ? '#445' : '#bbb', textAlign: 'center', overflow: 'hidden' }}>
141
+ {month.slice(5)}
142
+ </div>
143
+ ))}
144
+ </div>
145
+ </div>
146
+ )}
147
+
148
+ {/* Decay 위험 노트 */}
149
+ {data.decay.topDecaying.length > 0 && (
150
+ <div>
151
+ <div style={{ fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.5px', color: isDark ? '#667' : '#999', marginBottom: '4px' }}>
152
+ Most Decaying Notes
153
+ </div>
154
+ {data.decay.topDecaying.map((d, i) => {
155
+ const r = Math.round((d.retrievability ?? 0) * 100);
156
+ const barColor = r >= 50 ? '#10b981' : r >= 30 ? '#f59e0b' : '#ef4444';
157
+ return (
158
+ <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0', fontSize: '10px' }}>
159
+ <span style={{ flex: 1, color: isDark ? '#aab' : '#444', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
160
+ {d.title}
161
+ </span>
162
+ <div style={{ width: '60px', height: '4px', background: isDark ? 'rgba(100,120,255,0.08)' : 'rgba(0,0,0,0.04)', borderRadius: '2px', overflow: 'hidden' }}>
163
+ <div style={{ width: `${r}%`, height: '100%', background: barColor, borderRadius: '2px' }} />
164
+ </div>
165
+ <span style={{ width: '28px', color: barColor, fontSize: '9px', textAlign: 'right' }}>{r}%</span>
166
+ </div>
167
+ );
168
+ })}
169
+ </div>
170
+ )}
171
+ </div>
172
+ );
173
+ }