@@ -4,6 +4,7 @@ import { Application, extend } from "@pixi/react";
44import { Container as PixiContainer , Graphics as PixiGraphics } from "pixi.js" ;
55import { memo , useCallback , useEffect , useMemo , useRef } from "react" ;
66import { colors } from "./constants" ;
7+ import { getThemeColors } from "./theme-colors" ;
78import type { GraphCanvasProps , MemoryEntry } from "./types" ;
89
910// Register Pixi Graphics and Container so they can be used as JSX elements
@@ -32,7 +33,10 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
3233 onTouchMove,
3334 onTouchEnd,
3435 draggingNodeId,
36+ themeColors,
3537 } ) => {
38+ // Use theme colors or fallback to dark theme
39+ const colorPalette = themeColors || colors ;
3640 const containerRef = useRef < HTMLDivElement > ( null ) ;
3741 const isPanningRef = useRef ( false ) ;
3842 const currentHoveredRef = useRef < string | null > ( null ) ;
@@ -58,32 +62,40 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
5862 // ---------- Zoom bucket (reduces redraw frequency) ----------
5963 const zoomBucket = useMemo ( ( ) => Math . round ( zoom * 4 ) / 4 , [ zoom ] ) ;
6064
61- // Redraw layers only when their data changes ----------------------
62- useEffect ( ( ) => {
63- if ( gridG . current ) drawGrid ( gridG . current ) ;
64- } , [ panX , panY , zoom , width , height ] ) ;
65-
66- useEffect ( ( ) => {
67- if ( edgesG . current ) drawEdges ( edgesG . current ) ;
68- } , [ edgesG . current , edges , nodes , zoomBucket ] ) ;
69-
70- useEffect ( ( ) => {
71- if ( docsG . current ) drawDocuments ( docsG . current ) ;
72- } , [ docsG . current , nodes , zoomBucket ] ) ;
73-
74- useEffect ( ( ) => {
75- if ( memsG . current ) drawMemories ( memsG . current ) ;
76- } , [ memsG . current , nodes , zoomBucket ] ) ;
77-
78- // Apply pan & zoom via world transform instead of geometry rebuilds
79- useEffect ( ( ) => {
80- if ( worldContainerRef . current ) {
81- worldContainerRef . current . position . set ( panX , panY ) ;
82- worldContainerRef . current . scale . set ( zoom ) ;
65+ /* ---------- Color parsing ---------- */
66+ const toHexAlpha = ( input : string ) : { hex : number ; alpha : number } => {
67+ if ( ! input ) return { hex : 0xffffff , alpha : 1 } ;
68+ const str = input . trim ( ) . toLowerCase ( ) ;
69+ // rgba() or rgb()
70+ const rgbaMatch = str
71+ . replace ( / \s + / g, "" )
72+ . match ( / r g b a ? \( ( \d + ) , ( \d + ) , ( \d + ) (?: , ( \d * \. ? \d + ) ) ? \) / i) ;
73+ if ( rgbaMatch ) {
74+ const r = Number . parseInt ( rgbaMatch [ 1 ] || "0" ) ;
75+ const g = Number . parseInt ( rgbaMatch [ 2 ] || "0" ) ;
76+ const b = Number . parseInt ( rgbaMatch [ 3 ] || "0" ) ;
77+ const a =
78+ rgbaMatch [ 4 ] !== undefined ? Number . parseFloat ( rgbaMatch [ 4 ] ) : 1 ;
79+ return { hex : ( r << 16 ) + ( g << 8 ) + b , alpha : a } ;
8380 }
84- } , [ panX , panY , zoom ] ) ;
85-
86- // No bitmap caching – nothing to clean up
81+ // #rrggbb or #rrggbbaa
82+ if ( str . startsWith ( "#" ) ) {
83+ const hexBody = str . slice ( 1 ) ;
84+ if ( hexBody . length === 6 ) {
85+ return { hex : Number . parseInt ( hexBody , 16 ) , alpha : 1 } ;
86+ }
87+ if ( hexBody . length === 8 ) {
88+ const rgb = Number . parseInt ( hexBody . slice ( 0 , 6 ) , 16 ) ;
89+ const aByte = Number . parseInt ( hexBody . slice ( 6 , 8 ) , 16 ) ;
90+ return { hex : rgb , alpha : aByte / 255 } ;
91+ }
92+ }
93+ // 0xRRGGBB
94+ if ( str . startsWith ( "0x" ) ) {
95+ return { hex : Number . parseInt ( str , 16 ) , alpha : 1 } ;
96+ }
97+ return { hex : 0xffffff , alpha : 1 } ;
98+ } ;
8799
88100 /* ---------- Helpers ---------- */
89101 const getNodeAtPosition = useCallback (
@@ -128,8 +140,9 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
128140 ( g : PixiGraphics ) => {
129141 g . clear ( ) ;
130142
131- const gridColor = 0x94a3b8 ; // rgb(148,163,184)
132- const gridAlpha = 0.03 ;
143+ // Use theme-aware grid color
144+ const gridColorRGBA = colorPalette . grid ?. line || "rgba(148, 163, 184, 0.03)" ;
145+ const { hex : gridColor , alpha : gridAlpha } = toHexAlpha ( gridColorRGBA ) ;
133146 const gridSpacing = 100 * zoom ;
134147
135148 // panning offsets
@@ -153,44 +166,9 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
153166 // Stroke to render grid lines
154167 g . stroke ( ) ;
155168 } ,
156- [ panX , panY , zoom , width , height ] ,
169+ [ panX , panY , zoom , width , height , colorPalette ] ,
157170 ) ;
158171
159- /* ---------- Color parsing ---------- */
160- const toHexAlpha = ( input : string ) : { hex : number ; alpha : number } => {
161- if ( ! input ) return { hex : 0xffffff , alpha : 1 } ;
162- const str = input . trim ( ) . toLowerCase ( ) ;
163- // rgba() or rgb()
164- const rgbaMatch = str
165- . replace ( / \s + / g, "" )
166- . match ( / r g b a ? \( ( \d + ) , ( \d + ) , ( \d + ) (?: , ( \d * \. ? \d + ) ) ? \) / i) ;
167- if ( rgbaMatch ) {
168- const r = Number . parseInt ( rgbaMatch [ 1 ] || "0" ) ;
169- const g = Number . parseInt ( rgbaMatch [ 2 ] || "0" ) ;
170- const b = Number . parseInt ( rgbaMatch [ 3 ] || "0" ) ;
171- const a =
172- rgbaMatch [ 4 ] !== undefined ? Number . parseFloat ( rgbaMatch [ 4 ] ) : 1 ;
173- return { hex : ( r << 16 ) + ( g << 8 ) + b , alpha : a } ;
174- }
175- // #rrggbb or #rrggbbaa
176- if ( str . startsWith ( "#" ) ) {
177- const hexBody = str . slice ( 1 ) ;
178- if ( hexBody . length === 6 ) {
179- return { hex : Number . parseInt ( hexBody , 16 ) , alpha : 1 } ;
180- }
181- if ( hexBody . length === 8 ) {
182- const rgb = Number . parseInt ( hexBody . slice ( 0 , 6 ) , 16 ) ;
183- const aByte = Number . parseInt ( hexBody . slice ( 6 , 8 ) , 16 ) ;
184- return { hex : rgb , alpha : aByte / 255 } ;
185- }
186- }
187- // 0xRRGGBB
188- if ( str . startsWith ( "0x" ) ) {
189- return { hex : Number . parseInt ( str , 16 ) , alpha : 1 } ;
190- }
191- return { hex : 0xffffff , alpha : 1 } ;
192- } ;
193-
194172 const drawDocuments = useCallback (
195173 ( g : PixiGraphics ) => {
196174 g . clear ( ) ;
@@ -208,16 +186,16 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
208186
209187 // Choose colors similar to canvas version
210188 const fill = node . isDragging
211- ? colors . document . accent
189+ ? colorPalette . document . accent
212190 : node . isHovered
213- ? colors . document . secondary
214- : colors . document . primary ;
191+ ? colorPalette . document . secondary
192+ : colorPalette . document . primary ;
215193
216194 const strokeCol = node . isDragging
217- ? colors . document . glow
195+ ? colorPalette . document . glow
218196 : node . isHovered
219- ? colors . document . accent
220- : colors . document . border ;
197+ ? colorPalette . document . accent
198+ : colorPalette . document . border ;
221199
222200 const { hex : fillHex , alpha : fillAlpha } = toHexAlpha ( fill ) ;
223201 const { hex : strokeHex , alpha : strokeAlpha } = toHexAlpha ( strokeCol ) ;
@@ -255,7 +233,7 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
255233 }
256234 } ) ;
257235 } ,
258- [ nodes , zoom ] ,
236+ [ nodes , zoom , colorPalette ] ,
259237 ) ;
260238
261239 /* ---------- Memories layer ---------- */
@@ -290,27 +268,27 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
290268 Date . now ( ) - 1000 * 60 * 60 * 24 ;
291269
292270 // colours
293- let fillColor = colors . memory . primary ;
294- let borderColor = colors . memory . border ;
295- let glowColor = colors . memory . glow ;
271+ let fillColor = colorPalette . memory . primary ;
272+ let borderColor = colorPalette . memory . border ;
273+ let glowColor = colorPalette . memory . glow ;
296274
297275 if ( isForgotten ) {
298- fillColor = colors . status . forgotten ;
276+ fillColor = colorPalette . status . forgotten ;
299277 borderColor = "rgba(220,38,38,0.3)" ;
300278 glowColor = "rgba(220,38,38,0.2)" ;
301279 } else if ( expiringSoon ) {
302- borderColor = colors . status . expiring ;
303- glowColor = colors . accent . amber ;
280+ borderColor = colorPalette . status . expiring ;
281+ glowColor = colorPalette . accent . amber ;
304282 } else if ( isNew ) {
305- borderColor = colors . status . new ;
306- glowColor = colors . accent . emerald ;
283+ borderColor = colorPalette . status . new ;
284+ glowColor = colorPalette . accent . emerald ;
307285 }
308286
309287 if ( node . isDragging ) {
310- fillColor = colors . memory . accent ;
288+ fillColor = colorPalette . memory . accent ;
311289 borderColor = glowColor ;
312290 } else if ( node . isHovered ) {
313- fillColor = colors . memory . secondary ;
291+ fillColor = colorPalette . memory . secondary ;
314292 }
315293
316294 const { hex : fillHex , alpha : fillAlpha } = toHexAlpha ( fillColor ) ;
@@ -360,7 +338,7 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
360338 g . stroke ( ) ;
361339 } else if ( isNew ) {
362340 const { hex : dotHex , alpha : dotAlpha } = toHexAlpha (
363- colors . status . new ,
341+ colorPalette . status . new ,
364342 ) ;
365343 // Dot scales with node (GraphCanvas behaviour)
366344 const dotRadius = Math . max ( 2 , nodeSize * 0.15 ) ;
@@ -374,7 +352,7 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
374352 }
375353 } ) ;
376354 } ,
377- [ nodes , zoom ] ,
355+ [ nodes , zoom , colorPalette ] ,
378356 ) ;
379357
380358 /* ---------- Edges layer ---------- */
@@ -489,21 +467,21 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
489467 let lineWidth = Math . max ( 1 , edge . visualProps ?. thickness ?? 1 ) ;
490468 // Use opacity exactly as provided to match GraphCanvas behaviour
491469 let opacity = edge . visualProps . opacity ;
492- let col = edge . color || colors . connection . weak ;
470+ let col = edge . color || colorPalette . connection . weak ;
493471
494472 if ( edge . edgeType === "doc-memory" ) {
495473 lineWidth = 1 ;
496474 opacity = 0.9 ;
497- col = colors . connection . memory ;
475+ col = colorPalette . connection . memory ;
498476
499477 if ( useSimplified && opacity < 0.3 ) return ;
500478 } else if ( edge . edgeType === "doc-doc" ) {
501479 opacity = Math . max ( 0 , edge . similarity * 0.5 ) ;
502480 lineWidth = Math . max ( 1 , edge . similarity * 2 ) ;
503- col = colors . connection . medium ;
504- if ( edge . similarity > 0.85 ) col = colors . connection . strong ;
481+ col = colorPalette . connection . medium ;
482+ if ( edge . similarity > 0.85 ) col = colorPalette . connection . strong ;
505483 } else if ( edge . edgeType === "version" ) {
506- col = edge . color || colors . relations . updates ;
484+ col = edge . color || colorPalette . relations . updates ;
507485 opacity = 0.8 ;
508486 lineWidth = 2 ;
509487 }
@@ -584,7 +562,7 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
584562 }
585563 } ) ;
586564 } ,
587- [ edges , nodes , zoom , width , drawDashedQuadratic ] ,
565+ [ edges , nodes , zoom , width , drawDashedQuadratic , colorPalette ] ,
588566 ) ;
589567
590568 /* ---------- pointer handlers (unchanged) ---------- */
@@ -727,6 +705,31 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>(
727705 } ;
728706 } , [ ] ) ;
729707
708+ // Redraw layers only when their data changes ----------------------
709+ useEffect ( ( ) => {
710+ if ( gridG . current ) drawGrid ( gridG . current ) ;
711+ } , [ panX , panY , zoom , width , height , drawGrid ] ) ;
712+
713+ useEffect ( ( ) => {
714+ if ( edgesG . current ) drawEdges ( edgesG . current ) ;
715+ } , [ edges , nodes , zoomBucket , drawEdges ] ) ;
716+
717+ useEffect ( ( ) => {
718+ if ( docsG . current ) drawDocuments ( docsG . current ) ;
719+ } , [ nodes , zoomBucket , drawDocuments ] ) ;
720+
721+ useEffect ( ( ) => {
722+ if ( memsG . current ) drawMemories ( memsG . current ) ;
723+ } , [ nodes , zoomBucket , drawMemories ] ) ;
724+
725+ // Apply pan & zoom via world transform instead of geometry rebuilds
726+ useEffect ( ( ) => {
727+ if ( worldContainerRef . current ) {
728+ worldContainerRef . current . position . set ( panX , panY ) ;
729+ worldContainerRef . current . scale . set ( zoom ) ;
730+ }
731+ } , [ panX , panY , zoom ] ) ;
732+
730733 return (
731734 < div
732735 className = "absolute inset-0"
0 commit comments