claude-cortex 1.0.0 → 1.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 (79) hide show
  1. package/dashboard/README.md +36 -0
  2. package/dashboard/components.json +22 -0
  3. package/dashboard/eslint.config.mjs +18 -0
  4. package/dashboard/next.config.ts +7 -0
  5. package/dashboard/package-lock.json +7784 -0
  6. package/dashboard/package.json +42 -0
  7. package/dashboard/postcss.config.mjs +7 -0
  8. package/dashboard/public/file.svg +1 -0
  9. package/dashboard/public/globe.svg +1 -0
  10. package/dashboard/public/next.svg +1 -0
  11. package/dashboard/public/vercel.svg +1 -0
  12. package/dashboard/public/window.svg +1 -0
  13. package/dashboard/src/app/favicon.ico +0 -0
  14. package/dashboard/src/app/globals.css +125 -0
  15. package/dashboard/src/app/layout.tsx +35 -0
  16. package/dashboard/src/app/page.tsx +338 -0
  17. package/dashboard/src/components/Providers.tsx +27 -0
  18. package/dashboard/src/components/brain/ActivityPulseSystem.tsx +229 -0
  19. package/dashboard/src/components/brain/BrainMesh.tsx +118 -0
  20. package/dashboard/src/components/brain/BrainRegions.tsx +254 -0
  21. package/dashboard/src/components/brain/BrainScene.tsx +255 -0
  22. package/dashboard/src/components/brain/CategoryLabels.tsx +103 -0
  23. package/dashboard/src/components/brain/CoreSphere.tsx +215 -0
  24. package/dashboard/src/components/brain/DataFlowParticles.tsx +123 -0
  25. package/dashboard/src/components/brain/DataStreamRings.tsx +161 -0
  26. package/dashboard/src/components/brain/ElectronFlow.tsx +323 -0
  27. package/dashboard/src/components/brain/HolographicGrid.tsx +235 -0
  28. package/dashboard/src/components/brain/MemoryLinks.tsx +271 -0
  29. package/dashboard/src/components/brain/MemoryNode.tsx +245 -0
  30. package/dashboard/src/components/brain/NeuralPathways.tsx +441 -0
  31. package/dashboard/src/components/brain/SynapseNodes.tsx +306 -0
  32. package/dashboard/src/components/brain/TimelineControls.tsx +205 -0
  33. package/dashboard/src/components/chip/ChipScene.tsx +497 -0
  34. package/dashboard/src/components/chip/ChipSubstrate.tsx +238 -0
  35. package/dashboard/src/components/chip/CortexCore.tsx +210 -0
  36. package/dashboard/src/components/chip/DataBus.tsx +416 -0
  37. package/dashboard/src/components/chip/MemoryCell.tsx +225 -0
  38. package/dashboard/src/components/chip/MemoryGrid.tsx +328 -0
  39. package/dashboard/src/components/chip/QuantumCell.tsx +316 -0
  40. package/dashboard/src/components/chip/SectionLabel.tsx +113 -0
  41. package/dashboard/src/components/chip/index.ts +14 -0
  42. package/dashboard/src/components/controls/ControlPanel.tsx +100 -0
  43. package/dashboard/src/components/dashboard/StatsPanel.tsx +164 -0
  44. package/dashboard/src/components/debug/ActivityLog.tsx +238 -0
  45. package/dashboard/src/components/debug/DebugPanel.tsx +101 -0
  46. package/dashboard/src/components/debug/QueryTester.tsx +192 -0
  47. package/dashboard/src/components/debug/RelationshipGraph.tsx +403 -0
  48. package/dashboard/src/components/debug/SqlConsole.tsx +313 -0
  49. package/dashboard/src/components/memory/MemoryDetail.tsx +325 -0
  50. package/dashboard/src/components/ui/button.tsx +62 -0
  51. package/dashboard/src/components/ui/card.tsx +92 -0
  52. package/dashboard/src/components/ui/input.tsx +21 -0
  53. package/dashboard/src/hooks/useDebouncedValue.ts +24 -0
  54. package/dashboard/src/hooks/useMemories.ts +276 -0
  55. package/dashboard/src/hooks/useSuggestions.ts +46 -0
  56. package/dashboard/src/lib/category-colors.ts +84 -0
  57. package/dashboard/src/lib/position-algorithm.ts +177 -0
  58. package/dashboard/src/lib/simplex-noise.ts +217 -0
  59. package/dashboard/src/lib/store.ts +88 -0
  60. package/dashboard/src/lib/utils.ts +6 -0
  61. package/dashboard/src/lib/websocket.ts +216 -0
  62. package/dashboard/src/types/memory.ts +73 -0
  63. package/dashboard/tsconfig.json +34 -0
  64. package/dist/api/control.d.ts +27 -0
  65. package/dist/api/control.d.ts.map +1 -0
  66. package/dist/api/control.js +60 -0
  67. package/dist/api/control.js.map +1 -0
  68. package/dist/api/visualization-server.d.ts.map +1 -1
  69. package/dist/api/visualization-server.js +109 -2
  70. package/dist/api/visualization-server.js.map +1 -1
  71. package/dist/index.d.ts +2 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +80 -4
  74. package/dist/index.js.map +1 -1
  75. package/dist/memory/store.d.ts +6 -0
  76. package/dist/memory/store.d.ts.map +1 -1
  77. package/dist/memory/store.js +14 -0
  78. package/dist/memory/store.js.map +1 -1
  79. package/package.json +7 -3
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export { Input }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Debounce Hook
3
+ *
4
+ * Delays updating a value until after a specified delay has passed
5
+ * since the last time the value changed.
6
+ */
7
+
8
+ import { useState, useEffect } from 'react';
9
+
10
+ export function useDebouncedValue<T>(value: T, delay: number = 300): T {
11
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
12
+
13
+ useEffect(() => {
14
+ const timer = setTimeout(() => {
15
+ setDebouncedValue(value);
16
+ }, delay);
17
+
18
+ return () => {
19
+ clearTimeout(timer);
20
+ };
21
+ }, [value, delay]);
22
+
23
+ return debouncedValue;
24
+ }
@@ -0,0 +1,276 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Memory Data Hooks
5
+ * TanStack Query hooks for fetching memory data
6
+ *
7
+ * Uses WebSocket for real-time updates with polling as fallback.
8
+ */
9
+
10
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
11
+ import { Memory, MemoryStats, MemoryLink } from '@/types/memory';
12
+ import { useMemoryWebSocket } from '@/lib/websocket';
13
+
14
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
15
+
16
+ // Pagination metadata from API
17
+ export interface PaginationInfo {
18
+ offset: number;
19
+ limit: number;
20
+ total: number;
21
+ hasMore: boolean;
22
+ }
23
+
24
+ // Paginated response from API
25
+ interface PaginatedMemoriesResponse {
26
+ memories: Memory[];
27
+ pagination: PaginationInfo;
28
+ }
29
+
30
+ // Fetch memories with pagination support
31
+ async function fetchMemories(options?: {
32
+ project?: string;
33
+ type?: string;
34
+ category?: string;
35
+ limit?: number;
36
+ offset?: number;
37
+ mode?: 'recent' | 'important' | 'search';
38
+ query?: string;
39
+ }): Promise<PaginatedMemoriesResponse> {
40
+ const params = new URLSearchParams();
41
+ if (options?.project) params.set('project', options.project);
42
+ if (options?.type) params.set('type', options.type);
43
+ if (options?.category) params.set('category', options.category);
44
+ if (options?.limit) params.set('limit', options.limit.toString());
45
+ if (options?.offset) params.set('offset', options.offset.toString());
46
+ if (options?.mode) params.set('mode', options.mode);
47
+ if (options?.query) params.set('query', options.query);
48
+
49
+ const response = await fetch(`${API_BASE}/api/memories?${params}`);
50
+ if (!response.ok) throw new Error('Failed to fetch memories');
51
+ return response.json();
52
+ }
53
+
54
+ // Fetch memory stats
55
+ async function fetchStats(project?: string): Promise<MemoryStats> {
56
+ const params = project ? `?project=${project}` : '';
57
+ const response = await fetch(`${API_BASE}/api/stats${params}`);
58
+ if (!response.ok) throw new Error('Failed to fetch stats');
59
+ return response.json();
60
+ }
61
+
62
+ // Fetch memory links
63
+ async function fetchLinks(project?: string): Promise<MemoryLink[]> {
64
+ const params = project ? `?project=${project}` : '';
65
+ const response = await fetch(`${API_BASE}/api/links${params}`);
66
+ if (!response.ok) throw new Error('Failed to fetch links');
67
+ return response.json();
68
+ }
69
+
70
+ // Project info from API
71
+ export interface ProjectInfo {
72
+ project: string | null;
73
+ memory_count: number;
74
+ label: string;
75
+ }
76
+
77
+ // Fetch list of projects
78
+ async function fetchProjects(): Promise<{ projects: ProjectInfo[] }> {
79
+ const response = await fetch(`${API_BASE}/api/projects`);
80
+ if (!response.ok) throw new Error('Failed to fetch projects');
81
+ return response.json();
82
+ }
83
+
84
+ // Access a memory (reinforce)
85
+ async function accessMemory(id: number): Promise<Memory> {
86
+ const response = await fetch(`${API_BASE}/api/memories/${id}/access`, {
87
+ method: 'POST',
88
+ });
89
+ if (!response.ok) throw new Error('Failed to access memory');
90
+ return response.json();
91
+ }
92
+
93
+ // Trigger consolidation
94
+ async function triggerConsolidation(): Promise<{
95
+ success: boolean;
96
+ consolidated: number;
97
+ decayed: number;
98
+ deleted: number;
99
+ }> {
100
+ const response = await fetch(`${API_BASE}/api/consolidate`, {
101
+ method: 'POST',
102
+ });
103
+ if (!response.ok) throw new Error('Failed to consolidate');
104
+ return response.json();
105
+ }
106
+
107
+ // Hook: Get all memories with pagination
108
+ // Polling is reduced because WebSocket handles real-time updates
109
+ export function useMemories(options?: {
110
+ project?: string;
111
+ type?: string;
112
+ category?: string;
113
+ limit?: number;
114
+ offset?: number;
115
+ mode?: 'recent' | 'important' | 'search';
116
+ query?: string;
117
+ }) {
118
+ const query = useQuery({
119
+ queryKey: ['memories', options],
120
+ queryFn: () => fetchMemories(options),
121
+ refetchInterval: 30000, // Fallback poll every 30 seconds (WebSocket handles real-time)
122
+ });
123
+
124
+ // Extract memories array and pagination from response
125
+ return {
126
+ ...query,
127
+ data: query.data?.memories,
128
+ pagination: query.data?.pagination,
129
+ };
130
+ }
131
+
132
+ // Hook: Get memory stats
133
+ export function useStats(project?: string) {
134
+ return useQuery({
135
+ queryKey: ['stats', project],
136
+ queryFn: () => fetchStats(project),
137
+ refetchInterval: 30000, // Fallback poll every 30 seconds
138
+ });
139
+ }
140
+
141
+ // Hook: Get memory links
142
+ export function useMemoryLinks(project?: string) {
143
+ return useQuery({
144
+ queryKey: ['links', project],
145
+ queryFn: () => fetchLinks(project),
146
+ refetchInterval: 60000, // Fallback poll every 60 seconds
147
+ });
148
+ }
149
+
150
+ // Hook: Get list of projects
151
+ export function useProjects() {
152
+ return useQuery({
153
+ queryKey: ['projects'],
154
+ queryFn: fetchProjects,
155
+ refetchInterval: 60000, // Refresh project list every minute
156
+ });
157
+ }
158
+
159
+ // Hook: Combined memories with WebSocket real-time updates
160
+ export function useMemoriesWithRealtime(options?: {
161
+ project?: string;
162
+ type?: string;
163
+ category?: string;
164
+ limit?: number;
165
+ offset?: number;
166
+ mode?: 'recent' | 'important' | 'search';
167
+ query?: string;
168
+ }) {
169
+ // Connect to WebSocket for real-time updates
170
+ const ws = useMemoryWebSocket();
171
+
172
+ // Fetch memories with reduced polling (WebSocket handles most updates)
173
+ const memories = useMemories(options);
174
+
175
+ return {
176
+ ...memories,
177
+ isConnected: ws.isConnected,
178
+ lastEvent: ws.lastEvent,
179
+ };
180
+ }
181
+
182
+ // Hook: Access/reinforce a memory
183
+ export function useAccessMemory() {
184
+ const queryClient = useQueryClient();
185
+
186
+ return useMutation({
187
+ mutationFn: accessMemory,
188
+ onSuccess: () => {
189
+ // Invalidate memories to trigger refetch
190
+ queryClient.invalidateQueries({ queryKey: ['memories'] });
191
+ queryClient.invalidateQueries({ queryKey: ['stats'] });
192
+ },
193
+ });
194
+ }
195
+
196
+ // Hook: Trigger consolidation
197
+ export function useConsolidate() {
198
+ const queryClient = useQueryClient();
199
+
200
+ return useMutation({
201
+ mutationFn: triggerConsolidation,
202
+ onSuccess: () => {
203
+ queryClient.invalidateQueries({ queryKey: ['memories'] });
204
+ queryClient.invalidateQueries({ queryKey: ['stats'] });
205
+ },
206
+ });
207
+ }
208
+
209
+ // ============================================
210
+ // CONTROL API
211
+ // ============================================
212
+
213
+ // Control status response
214
+ export interface ControlStatus {
215
+ paused: boolean;
216
+ uptime: number;
217
+ uptimeFormatted: string;
218
+ }
219
+
220
+ // Fetch control status
221
+ async function fetchControlStatus(): Promise<ControlStatus> {
222
+ const response = await fetch(`${API_BASE}/api/control/status`);
223
+ if (!response.ok) throw new Error('Failed to fetch control status');
224
+ return response.json();
225
+ }
226
+
227
+ // Pause memory creation
228
+ async function pauseMemoryCreation(): Promise<{ paused: boolean }> {
229
+ const response = await fetch(`${API_BASE}/api/control/pause`, {
230
+ method: 'POST',
231
+ });
232
+ if (!response.ok) throw new Error('Failed to pause');
233
+ return response.json();
234
+ }
235
+
236
+ // Resume memory creation
237
+ async function resumeMemoryCreation(): Promise<{ paused: boolean }> {
238
+ const response = await fetch(`${API_BASE}/api/control/resume`, {
239
+ method: 'POST',
240
+ });
241
+ if (!response.ok) throw new Error('Failed to resume');
242
+ return response.json();
243
+ }
244
+
245
+ // Hook: Get control status
246
+ export function useControlStatus() {
247
+ return useQuery({
248
+ queryKey: ['control-status'],
249
+ queryFn: fetchControlStatus,
250
+ refetchInterval: 10000, // Poll every 10 seconds for uptime updates
251
+ });
252
+ }
253
+
254
+ // Hook: Pause memory creation
255
+ export function usePauseMemory() {
256
+ const queryClient = useQueryClient();
257
+
258
+ return useMutation({
259
+ mutationFn: pauseMemoryCreation,
260
+ onSuccess: () => {
261
+ queryClient.invalidateQueries({ queryKey: ['control-status'] });
262
+ },
263
+ });
264
+ }
265
+
266
+ // Hook: Resume memory creation
267
+ export function useResumeMemory() {
268
+ const queryClient = useQueryClient();
269
+
270
+ return useMutation({
271
+ mutationFn: resumeMemoryCreation,
272
+ onSuccess: () => {
273
+ queryClient.invalidateQueries({ queryKey: ['control-status'] });
274
+ },
275
+ });
276
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Search Suggestions Hook
3
+ *
4
+ * Fetches autocomplete suggestions for the search input.
5
+ */
6
+
7
+ import { useQuery } from '@tanstack/react-query';
8
+ import { useDebouncedValue } from './useDebouncedValue';
9
+
10
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
11
+
12
+ interface Suggestion {
13
+ text: string;
14
+ type: 'title' | 'category' | 'project';
15
+ count: number;
16
+ }
17
+
18
+ async function fetchSuggestions(query: string): Promise<Suggestion[]> {
19
+ if (!query || query.length < 2) {
20
+ return [];
21
+ }
22
+
23
+ const response = await fetch(
24
+ `${API_URL}/api/suggestions?q=${encodeURIComponent(query)}&limit=8`
25
+ );
26
+
27
+ if (!response.ok) {
28
+ throw new Error('Failed to fetch suggestions');
29
+ }
30
+
31
+ const data = await response.json();
32
+ return data.suggestions;
33
+ }
34
+
35
+ export function useSuggestions(query: string) {
36
+ // Debounce the query to avoid too many requests
37
+ const debouncedQuery = useDebouncedValue(query, 200);
38
+
39
+ return useQuery({
40
+ queryKey: ['suggestions', debouncedQuery],
41
+ queryFn: () => fetchSuggestions(debouncedQuery),
42
+ enabled: debouncedQuery.length >= 2,
43
+ staleTime: 30000, // Cache for 30 seconds
44
+ refetchOnWindowFocus: false,
45
+ });
46
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Category Colors
3
+ * Visual color mappings for memory categories
4
+ *
5
+ * Includes both classic (cool blue/purple) and Jarvis (warm gold/orange) palettes
6
+ */
7
+
8
+ import { MemoryCategory, MemoryType } from '@/types/memory';
9
+
10
+ // Classic color palette (cool blues and purples)
11
+ export const CATEGORY_COLORS: Record<MemoryCategory, string> = {
12
+ architecture: '#3B82F6', // Blue
13
+ pattern: '#8B5CF6', // Purple
14
+ preference: '#EC4899', // Pink
15
+ error: '#EF4444', // Red
16
+ context: '#10B981', // Green
17
+ learning: '#F59E0B', // Amber
18
+ todo: '#F97316', // Orange
19
+ note: '#6B7280', // Gray
20
+ relationship: '#06B6D4', // Cyan
21
+ custom: '#A855F7', // Violet
22
+ };
23
+
24
+ export const TYPE_COLORS: Record<MemoryType, string> = {
25
+ short_term: '#F97316', // Orange
26
+ long_term: '#3B82F6', // Blue
27
+ episodic: '#8B5CF6', // Purple
28
+ };
29
+
30
+ // Jarvis color palette (warm gold/orange holographic style)
31
+ export const JARVIS_CATEGORY_COLORS: Record<MemoryCategory, string> = {
32
+ architecture: '#FFD700', // Bright gold
33
+ pattern: '#FFB347', // Warm gold
34
+ preference: '#FFA500', // Pure orange
35
+ error: '#FF6B6B', // Keep red-ish for errors
36
+ context: '#FFC080', // Soft peach
37
+ learning: '#FFE4B5', // Moccasin
38
+ todo: '#FF8C00', // Deep orange
39
+ note: '#FFCC66', // Light amber
40
+ relationship: '#00D4FF', // Cyan accent
41
+ custom: '#FFB347', // Warm gold
42
+ };
43
+
44
+ export const JARVIS_TYPE_COLORS: Record<MemoryType, string> = {
45
+ short_term: '#FFD700', // Bright gold (front)
46
+ episodic: '#FFB347', // Warm gold (middle)
47
+ long_term: '#FF8C00', // Deep orange (back)
48
+ };
49
+
50
+ // Color mode toggle - defaults to Jarvis mode
51
+ let useJarvisColors = true;
52
+
53
+ export function setUseJarvisColors(value: boolean): void {
54
+ useJarvisColors = value;
55
+ }
56
+
57
+ export function getUseJarvisColors(): boolean {
58
+ return useJarvisColors;
59
+ }
60
+
61
+ export function getCategoryColor(category: MemoryCategory): string {
62
+ if (useJarvisColors) {
63
+ return JARVIS_CATEGORY_COLORS[category] || JARVIS_CATEGORY_COLORS.custom;
64
+ }
65
+ return CATEGORY_COLORS[category] || CATEGORY_COLORS.custom;
66
+ }
67
+
68
+ export function getTypeColor(type: MemoryType): string {
69
+ if (useJarvisColors) {
70
+ return JARVIS_TYPE_COLORS[type] || JARVIS_TYPE_COLORS.short_term;
71
+ }
72
+ return TYPE_COLORS[type] || TYPE_COLORS.short_term;
73
+ }
74
+
75
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
76
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
77
+ return result
78
+ ? {
79
+ r: parseInt(result[1], 16) / 255,
80
+ g: parseInt(result[2], 16) / 255,
81
+ b: parseInt(result[3], 16) / 255,
82
+ }
83
+ : { r: 1, g: 1, b: 1 };
84
+ }