@backbay/glia-three 0.2.0-alpha.2

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 (172) hide show
  1. package/package.json +43 -0
  2. package/src/environment/AuroraLayer/AuroraLayer.stories.tsx +43 -0
  3. package/src/environment/AuroraLayer/AuroraLayer.tsx +200 -0
  4. package/src/environment/AuroraLayer/index.ts +2 -0
  5. package/src/environment/AuroraLayer/types.ts +30 -0
  6. package/src/environment/EnvironmentLayer/EnvironmentLayer.stories.tsx +262 -0
  7. package/src/environment/EnvironmentLayer/EnvironmentLayer.tsx +105 -0
  8. package/src/environment/EnvironmentLayer/index.ts +4 -0
  9. package/src/environment/EnvironmentLayer/presets.ts +128 -0
  10. package/src/environment/FogLayer/FogLayer.stories.tsx +83 -0
  11. package/src/environment/FogLayer/FogLayer.tsx +113 -0
  12. package/src/environment/FogLayer/index.ts +2 -0
  13. package/src/environment/FogLayer/types.ts +45 -0
  14. package/src/environment/VolumetricLight/VolumetricLight.stories.tsx +55 -0
  15. package/src/environment/VolumetricLight/VolumetricLight.tsx +188 -0
  16. package/src/environment/VolumetricLight/index.ts +2 -0
  17. package/src/environment/VolumetricLight/types.ts +33 -0
  18. package/src/environment/WeatherLayer/WeatherLayer.stories.tsx +348 -0
  19. package/src/environment/WeatherLayer/WeatherLayer.tsx +266 -0
  20. package/src/environment/WeatherLayer/cinematicCanvas.tsx +809 -0
  21. package/src/environment/WeatherLayer/colors.ts +27 -0
  22. package/src/environment/WeatherLayer/index.ts +4 -0
  23. package/src/environment/WeatherLayer/leafPresets.ts +12 -0
  24. package/src/environment/WeatherLayer/particles.ts +227 -0
  25. package/src/environment/WeatherLayer/types.ts +140 -0
  26. package/src/environment/index.ts +17 -0
  27. package/src/environment/shared/index.ts +2 -0
  28. package/src/environment/shared/noise.ts +10 -0
  29. package/src/environment/shared/performance.ts +33 -0
  30. package/src/environment/shared/types.ts +26 -0
  31. package/src/index.ts +2 -0
  32. package/src/lib/utils.ts +6 -0
  33. package/src/lib/vision-types.ts +84 -0
  34. package/src/three/AgentConsole/AgentConsole.stories.tsx +397 -0
  35. package/src/three/AgentConsole/AgentConsole.tsx +195 -0
  36. package/src/three/AgentConsole/ConsoleChat.tsx +237 -0
  37. package/src/three/AgentConsole/FocusConstellation.tsx +297 -0
  38. package/src/three/AgentConsole/GlyphAvatar.stories.tsx +110 -0
  39. package/src/three/AgentConsole/GlyphAvatar.tsx +117 -0
  40. package/src/three/AgentConsole/QuickActions.tsx +111 -0
  41. package/src/three/AgentConsole/index.ts +41 -0
  42. package/src/three/AgentConsole/types.ts +241 -0
  43. package/src/three/AmbientField/AmbientField.stories.tsx +290 -0
  44. package/src/three/AmbientField/AmbientField.tsx +307 -0
  45. package/src/three/AmbientField/BackbayFieldBus.ts +326 -0
  46. package/src/three/AmbientField/FieldProvider.tsx +83 -0
  47. package/src/three/AmbientField/index.ts +37 -0
  48. package/src/three/AmbientField/shaders/constellation.ts +384 -0
  49. package/src/three/AmbientField/types.ts +174 -0
  50. package/src/three/AttackGraph/AttackGraph.stories.tsx +144 -0
  51. package/src/three/AttackGraph/AttackGraph.tsx +325 -0
  52. package/src/three/AttackGraph/index.ts +19 -0
  53. package/src/three/AttackGraph/types.ts +97 -0
  54. package/src/three/AuditTrail/AuditTrail.stories.tsx +567 -0
  55. package/src/three/AuditTrail/AuditTrail.tsx +644 -0
  56. package/src/three/AuditTrail/index.ts +33 -0
  57. package/src/three/AuditTrail/types.ts +192 -0
  58. package/src/three/CrystallineOrganism/Breadcrumb.tsx +61 -0
  59. package/src/three/CrystallineOrganism/CrystallineOrganism.stories.tsx +509 -0
  60. package/src/three/CrystallineOrganism/CrystallineOrganism.tsx +273 -0
  61. package/src/three/CrystallineOrganism/LatticeEdge.tsx +69 -0
  62. package/src/three/CrystallineOrganism/OrganismLattice.tsx +159 -0
  63. package/src/three/CrystallineOrganism/OrganismParticles.tsx +148 -0
  64. package/src/three/CrystallineOrganism/OrganismShell.tsx +277 -0
  65. package/src/three/CrystallineOrganism/constants.ts +161 -0
  66. package/src/three/CrystallineOrganism/index.ts +17 -0
  67. package/src/three/CrystallineOrganism/layouts/hexGrid.ts +85 -0
  68. package/src/three/CrystallineOrganism/layouts/index.ts +1 -0
  69. package/src/three/CrystallineOrganism/types.ts +167 -0
  70. package/src/three/CrystallineOrganism/useOrganismEmotion.ts +84 -0
  71. package/src/three/FirewallBarrier/FirewallBarrier.stories.tsx +167 -0
  72. package/src/three/FirewallBarrier/FirewallBarrier.tsx +259 -0
  73. package/src/three/FirewallBarrier/index.ts +14 -0
  74. package/src/three/FirewallBarrier/types.ts +52 -0
  75. package/src/three/Glyph/GlyphObject.stories.tsx +577 -0
  76. package/src/three/Glyph/GlyphObject.tsx +422 -0
  77. package/src/three/Glyph/index.ts +10 -0
  78. package/src/three/Glyph/types.ts +36 -0
  79. package/src/three/Glyph/useGlyphController.ts +231 -0
  80. package/src/three/Glyph/useGlyphEmotion.ts +70 -0
  81. package/src/three/Graph3D/Graph3D.stories.tsx +269 -0
  82. package/src/three/Graph3D/Graph3D.tsx +248 -0
  83. package/src/three/Graph3D/GraphEdge.tsx +79 -0
  84. package/src/three/Graph3D/GraphNode.tsx +239 -0
  85. package/src/three/Graph3D/types.ts +66 -0
  86. package/src/three/Graph3D/utils.ts +204 -0
  87. package/src/three/IntelFeed/IntelFeed.stories.tsx +168 -0
  88. package/src/three/IntelFeed/IntelFeed.tsx +284 -0
  89. package/src/three/IntelFeed/index.ts +14 -0
  90. package/src/three/IntelFeed/types.ts +56 -0
  91. package/src/three/MetricsGalaxy/MetricsGalaxy.tsx +484 -0
  92. package/src/three/MetricsGalaxy/index.ts +6 -0
  93. package/src/three/MetricsGalaxy/types.ts +26 -0
  94. package/src/three/NetworkTopology/NetworkTopology.stories.tsx +184 -0
  95. package/src/three/NetworkTopology/NetworkTopology.tsx +421 -0
  96. package/src/three/NetworkTopology/index.ts +34 -0
  97. package/src/three/NetworkTopology/types.ts +128 -0
  98. package/src/three/ParticleField/ParticleField.stories.tsx +162 -0
  99. package/src/three/ParticleField/ParticleField.tsx +81 -0
  100. package/src/three/ParticleField/index.ts +1 -0
  101. package/src/three/PermissionMatrix/PermissionMatrix.stories.tsx +475 -0
  102. package/src/three/PermissionMatrix/PermissionMatrix.tsx +380 -0
  103. package/src/three/PermissionMatrix/index.ts +15 -0
  104. package/src/three/PermissionMatrix/types.ts +54 -0
  105. package/src/three/QuantumField/ConstellationField.tsx +238 -0
  106. package/src/three/QuantumField/FieldBus.ts +349 -0
  107. package/src/three/QuantumField/FieldLayer.tsx +430 -0
  108. package/src/three/QuantumField/FieldProvider.tsx +460 -0
  109. package/src/three/QuantumField/PcbField.tsx +406 -0
  110. package/src/three/QuantumField/QuantumField.stories.tsx +1155 -0
  111. package/src/three/QuantumField/TrailRTT.ts +212 -0
  112. package/src/three/QuantumField/WaterField.tsx +226 -0
  113. package/src/three/QuantumField/WaterSimRTT.ts +283 -0
  114. package/src/three/QuantumField/domMapping.ts +185 -0
  115. package/src/three/QuantumField/index.ts +110 -0
  116. package/src/three/QuantumField/styles/index.ts +9 -0
  117. package/src/three/QuantumField/styles/styleA.ts +526 -0
  118. package/src/three/QuantumField/styles/styleB.ts +1210 -0
  119. package/src/three/QuantumField/styles/styleC.ts +266 -0
  120. package/src/three/QuantumField/themes.ts +211 -0
  121. package/src/three/QuantumField/types.ts +380 -0
  122. package/src/three/SOCCommandCenter/SOCCommandCenter.stories.tsx +591 -0
  123. package/src/three/SOCCommandCenter/SOCCommandCenter.tsx +248 -0
  124. package/src/three/SOCCommandCenter/index.ts +26 -0
  125. package/src/three/SOCCommandCenter/types.ts +201 -0
  126. package/src/three/SecurityDashboard/SecurityDashboard.stories.tsx +508 -0
  127. package/src/three/SecurityDashboard/SecurityDashboard.tsx +507 -0
  128. package/src/three/SecurityDashboard/index.ts +37 -0
  129. package/src/three/SecurityDashboard/types.ts +143 -0
  130. package/src/three/SecurityShield/SecurityShield.stories.tsx +257 -0
  131. package/src/three/SecurityShield/SecurityShield.tsx +502 -0
  132. package/src/three/SecurityShield/index.ts +25 -0
  133. package/src/three/SecurityShield/types.ts +64 -0
  134. package/src/three/Sentinel/AvatarMode.tsx +578 -0
  135. package/src/three/Sentinel/AvatarRenderer.tsx +199 -0
  136. package/src/three/Sentinel/CameraPip.tsx +127 -0
  137. package/src/three/Sentinel/CardinalItem.tsx +83 -0
  138. package/src/three/Sentinel/CardinalMenu.tsx +370 -0
  139. package/src/three/Sentinel/DockedMiniOrb.tsx +146 -0
  140. package/src/three/Sentinel/RadialSubmenu.tsx +273 -0
  141. package/src/three/Sentinel/SentinelConversation.tsx +802 -0
  142. package/src/three/Sentinel/SentinelOrb.tsx +316 -0
  143. package/src/three/Sentinel/SentinelOverlay.tsx +146 -0
  144. package/src/three/Sentinel/SentinelProvider.tsx +145 -0
  145. package/src/three/Sentinel/SentinelTether.tsx +182 -0
  146. package/src/three/Sentinel/SigilPlaceholder.tsx +176 -0
  147. package/src/three/Sentinel/VerticalSubmenu.tsx +150 -0
  148. package/src/three/Sentinel/index.ts +145 -0
  149. package/src/three/Sentinel/sentinelStore.ts +196 -0
  150. package/src/three/Sentinel/types.ts +403 -0
  151. package/src/three/Sentinel/useCameraPermission.ts +153 -0
  152. package/src/three/Sentinel/useThrowPhysics.ts +220 -0
  153. package/src/three/SpatialWorkspace/CyntraWorkspace.tsx +84 -0
  154. package/src/three/SpatialWorkspace/JobCluster.tsx +281 -0
  155. package/src/three/SpatialWorkspace/NodeGraph.tsx +236 -0
  156. package/src/three/SpatialWorkspace/ReceiptOrbit.tsx +368 -0
  157. package/src/three/SpatialWorkspace/SpatialWorkspace.stories.tsx +547 -0
  158. package/src/three/SpatialWorkspace/SpatialWorkspace.tsx +428 -0
  159. package/src/three/SpatialWorkspace/TrustRings.tsx +228 -0
  160. package/src/three/SpatialWorkspace/adapters.ts +353 -0
  161. package/src/three/SpatialWorkspace/index.ts +85 -0
  162. package/src/three/SpatialWorkspace/nexusAdapter.ts +182 -0
  163. package/src/three/SpatialWorkspace/types.ts +389 -0
  164. package/src/three/ThreatRadar/ThreatRadar.stories.tsx +451 -0
  165. package/src/three/ThreatRadar/ThreatRadar.tsx +542 -0
  166. package/src/three/ThreatRadar/index.ts +8 -0
  167. package/src/three/ThreatRadar/types.ts +90 -0
  168. package/src/three/ThreeErrorBoundary/ThreeErrorBoundary.tsx +235 -0
  169. package/src/three/ThreeErrorBoundary/index.ts +5 -0
  170. package/src/three/index.ts +56 -0
  171. package/tsconfig.json +20 -0
  172. package/tsup.config.ts +21 -0
@@ -0,0 +1,368 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ReceiptOrbit - Status ring visualization for receipts
5
+ *
6
+ * Displays receipt status as colored arc segments in a ring around
7
+ * the parent entity. Each segment's arc length is proportional to
8
+ * the count of receipts in that status.
9
+ *
10
+ * Colors: passed=#00ff88, failed=#ff0055, pending=#00f0ff
11
+ */
12
+
13
+ import * as React from "react";
14
+ import { useFrame } from "@react-three/fiber";
15
+ import { Html } from "@react-three/drei";
16
+ import * as THREE from "three";
17
+ import type { Receipt, ReceiptStatus } from "@backbay/contract";
18
+ import { RECEIPT_VISUALS } from "./types";
19
+ import type { ReceiptOrbitProps, TopologySlice } from "./types";
20
+
21
+ // -----------------------------------------------------------------------------
22
+ // Status Counts Type
23
+ // -----------------------------------------------------------------------------
24
+
25
+ interface StatusCounts {
26
+ passed: number;
27
+ failed: number;
28
+ pending: number;
29
+ }
30
+
31
+ // -----------------------------------------------------------------------------
32
+ // Arc Segment Component
33
+ // -----------------------------------------------------------------------------
34
+
35
+ interface ArcSegmentProps {
36
+ innerRadius: number;
37
+ outerRadius: number;
38
+ thetaStart: number;
39
+ thetaLength: number;
40
+ color: string;
41
+ opacity: number;
42
+ emissiveIntensity: number;
43
+ pulse?: boolean;
44
+ }
45
+
46
+ function ArcSegment({
47
+ innerRadius,
48
+ outerRadius,
49
+ thetaStart,
50
+ thetaLength,
51
+ color,
52
+ opacity,
53
+ emissiveIntensity,
54
+ pulse = false,
55
+ }: ArcSegmentProps) {
56
+ const meshRef = React.useRef<THREE.Mesh>(null);
57
+ const baseIntensity = emissiveIntensity;
58
+
59
+ useFrame((state) => {
60
+ if (!meshRef.current || !pulse) return;
61
+
62
+ // Pulse the emissive intensity for failed segments
63
+ const material = meshRef.current.material as THREE.MeshStandardMaterial;
64
+ const pulseValue = Math.sin(state.clock.elapsedTime * 3) * 0.3 + 0.7;
65
+ material.emissiveIntensity = baseIntensity * pulseValue;
66
+ });
67
+
68
+ const colorObj = new THREE.Color(color);
69
+
70
+ // Don't render if arc length is effectively zero
71
+ if (thetaLength < 0.01) return null;
72
+
73
+ return (
74
+ <mesh ref={meshRef} rotation={[-Math.PI / 2, 0, 0]}>
75
+ <ringGeometry
76
+ args={[innerRadius, outerRadius, 32, 1, thetaStart, thetaLength]}
77
+ />
78
+ <meshStandardMaterial
79
+ color={colorObj}
80
+ transparent
81
+ opacity={opacity}
82
+ emissive={colorObj}
83
+ emissiveIntensity={emissiveIntensity}
84
+ side={THREE.DoubleSide}
85
+ metalness={0.3}
86
+ roughness={0.5}
87
+ />
88
+ </mesh>
89
+ );
90
+ }
91
+
92
+ // -----------------------------------------------------------------------------
93
+ // Status Ring Component
94
+ // -----------------------------------------------------------------------------
95
+
96
+ interface StatusRingProps {
97
+ counts: StatusCounts;
98
+ total: number;
99
+ radius: number;
100
+ thickness?: number;
101
+ }
102
+
103
+ function StatusRing({
104
+ counts,
105
+ total,
106
+ radius,
107
+ thickness = 0.05,
108
+ }: StatusRingProps) {
109
+ const innerRadius = radius - thickness;
110
+ const outerRadius = radius + thickness;
111
+
112
+ // Calculate arc angles for each status
113
+ // Order: passed first (starts at 0), then pending, then failed
114
+ const passedLength = total > 0 ? (counts.passed / total) * Math.PI * 2 : 0;
115
+ const pendingLength = total > 0 ? (counts.pending / total) * Math.PI * 2 : 0;
116
+ const failedLength = total > 0 ? (counts.failed / total) * Math.PI * 2 : 0;
117
+
118
+ const passedStart = 0;
119
+ const pendingStart = passedLength;
120
+ const failedStart = passedLength + pendingLength;
121
+
122
+ return (
123
+ <group>
124
+ {/* Passed segment */}
125
+ {counts.passed > 0 && (
126
+ <ArcSegment
127
+ innerRadius={innerRadius}
128
+ outerRadius={outerRadius}
129
+ thetaStart={passedStart}
130
+ thetaLength={passedLength}
131
+ color={RECEIPT_VISUALS.passed.color}
132
+ opacity={0.7}
133
+ emissiveIntensity={0.3}
134
+ />
135
+ )}
136
+
137
+ {/* Pending segment */}
138
+ {counts.pending > 0 && (
139
+ <ArcSegment
140
+ innerRadius={innerRadius}
141
+ outerRadius={outerRadius}
142
+ thetaStart={pendingStart}
143
+ thetaLength={pendingLength}
144
+ color={RECEIPT_VISUALS.pending.color}
145
+ opacity={0.5}
146
+ emissiveIntensity={0.2}
147
+ />
148
+ )}
149
+
150
+ {/* Failed segment - with pulse effect */}
151
+ {counts.failed > 0 && (
152
+ <ArcSegment
153
+ innerRadius={innerRadius}
154
+ outerRadius={outerRadius}
155
+ thetaStart={failedStart}
156
+ thetaLength={failedLength}
157
+ color={RECEIPT_VISUALS.failed.color}
158
+ opacity={0.8}
159
+ emissiveIntensity={0.5}
160
+ pulse
161
+ />
162
+ )}
163
+
164
+ {/* Subtle glow ring behind the segments */}
165
+ <mesh rotation={[-Math.PI / 2, 0, 0]}>
166
+ <ringGeometry args={[innerRadius - 0.02, outerRadius + 0.02, 64]} />
167
+ <meshBasicMaterial
168
+ color="#ffffff"
169
+ transparent
170
+ opacity={0.05}
171
+ side={THREE.DoubleSide}
172
+ />
173
+ </mesh>
174
+ </group>
175
+ );
176
+ }
177
+
178
+ // -----------------------------------------------------------------------------
179
+ // Status Tooltip Component
180
+ // -----------------------------------------------------------------------------
181
+
182
+ interface StatusTooltipProps {
183
+ counts: StatusCounts;
184
+ total: number;
185
+ }
186
+
187
+ function StatusTooltip({ counts, total }: StatusTooltipProps) {
188
+ return (
189
+ <Html center style={{ pointerEvents: "none" }}>
190
+ <div className="bg-black/90 text-white text-xs px-3 py-2 rounded font-mono whitespace-nowrap border border-white/10">
191
+ <div className="text-white/60 mb-1 text-center">
192
+ {total} receipt{total !== 1 ? "s" : ""}
193
+ </div>
194
+ <div className="flex gap-3">
195
+ {counts.passed > 0 && (
196
+ <span style={{ color: RECEIPT_VISUALS.passed.color }}>
197
+ {counts.passed} passed
198
+ </span>
199
+ )}
200
+ {counts.failed > 0 && (
201
+ <span style={{ color: RECEIPT_VISUALS.failed.color }}>
202
+ {counts.failed} failed
203
+ </span>
204
+ )}
205
+ {counts.pending > 0 && (
206
+ <span style={{ color: RECEIPT_VISUALS.pending.color }}>
207
+ {counts.pending} pending
208
+ </span>
209
+ )}
210
+ </div>
211
+ </div>
212
+ </Html>
213
+ );
214
+ }
215
+
216
+ // -----------------------------------------------------------------------------
217
+ // Receipt Orbit Container
218
+ // -----------------------------------------------------------------------------
219
+
220
+ export function ReceiptOrbit({
221
+ receipts,
222
+ parentPosition,
223
+ orbitRadius = 0.6,
224
+ selectedIds = [],
225
+ hoveredId = null,
226
+ onReceiptClick,
227
+ onReceiptHover,
228
+ onTopologyChange,
229
+ origin = [0, 0, 0],
230
+ }: ReceiptOrbitProps) {
231
+ const [isHovered, setIsHovered] = React.useState(false);
232
+
233
+ // Calculate status counts
234
+ const statusCounts = React.useMemo<StatusCounts>(
235
+ () => ({
236
+ passed: receipts.filter((r) => r.status === "passed").length,
237
+ failed: receipts.filter((r) => r.status === "failed").length,
238
+ pending: receipts.filter((r) => r.status === "pending").length,
239
+ }),
240
+ [receipts]
241
+ );
242
+
243
+ // Report topology - simplified to single receipt-ring node
244
+ const topologySlice = React.useMemo<TopologySlice>(() => {
245
+ if (receipts.length === 0) {
246
+ return { nodes: [], edges: [] };
247
+ }
248
+
249
+ // Determine dominant status for ring color
250
+ const dominantStatus: ReceiptStatus =
251
+ statusCounts.failed > 0
252
+ ? "failed"
253
+ : statusCounts.passed === receipts.length
254
+ ? "passed"
255
+ : "pending";
256
+
257
+ const ringPosition: [number, number, number] = [
258
+ parentPosition[0] + origin[0],
259
+ parentPosition[1] + origin[1],
260
+ parentPosition[2] + origin[2],
261
+ ];
262
+
263
+ const nodes = [
264
+ {
265
+ id: `receipt-ring-${receipts[0]?.job_id ?? "unknown"}`,
266
+ type: "receipt-ring",
267
+ label: `${receipts.length} receipts`,
268
+ position: ringPosition,
269
+ radius: orbitRadius,
270
+ color: RECEIPT_VISUALS[dominantStatus].color,
271
+ meta: {
272
+ counts: statusCounts,
273
+ receiptIds: receipts.map((r) => r.id),
274
+ },
275
+ },
276
+ ];
277
+
278
+ // Edge from job to the receipt ring
279
+ const edges = receipts[0]?.job_id
280
+ ? [
281
+ {
282
+ id: `receipt-ring-${receipts[0].job_id}-edge`,
283
+ from: receipts[0].job_id,
284
+ to: `receipt-ring-${receipts[0].job_id}`,
285
+ type: "receipt-ring-for-job",
286
+ color: RECEIPT_VISUALS[dominantStatus].color,
287
+ },
288
+ ]
289
+ : [];
290
+
291
+ return { nodes, edges };
292
+ }, [receipts, statusCounts, orbitRadius, parentPosition, origin]);
293
+
294
+ React.useEffect(() => {
295
+ if (!onTopologyChange) return;
296
+ onTopologyChange(topologySlice);
297
+ }, [onTopologyChange, topologySlice]);
298
+
299
+ // Handle pointer out
300
+ const handlePointerOut = React.useCallback(() => {
301
+ setIsHovered(false);
302
+ document.body.style.cursor = "auto";
303
+ if (onReceiptHover) {
304
+ onReceiptHover(null);
305
+ }
306
+ }, [onReceiptHover]);
307
+
308
+ if (receipts.length === 0) return null;
309
+
310
+ // Check if any receipt is selected
311
+ const hasSelection = selectedIds.some((id) =>
312
+ receipts.some((r) => r.id === id)
313
+ );
314
+
315
+ return (
316
+ <group position={parentPosition}>
317
+ {/* Invisible interaction mesh for hover/click */}
318
+ <mesh
319
+ rotation={[-Math.PI / 2, 0, 0]}
320
+ onClick={(e) => {
321
+ e.stopPropagation();
322
+ if (onReceiptClick && receipts.length > 0) {
323
+ onReceiptClick(receipts[0]);
324
+ }
325
+ }}
326
+ onPointerOver={(e) => {
327
+ e.stopPropagation();
328
+ setIsHovered(true);
329
+ document.body.style.cursor = "pointer";
330
+ if (onReceiptHover && receipts.length > 0) {
331
+ onReceiptHover(receipts[0]);
332
+ }
333
+ }}
334
+ onPointerOut={handlePointerOut}
335
+ >
336
+ <ringGeometry args={[orbitRadius - 0.1, orbitRadius + 0.1, 64]} />
337
+ <meshBasicMaterial transparent opacity={0} side={THREE.DoubleSide} />
338
+ </mesh>
339
+
340
+ {/* Status ring with segments */}
341
+ <StatusRing
342
+ counts={statusCounts}
343
+ total={receipts.length}
344
+ radius={orbitRadius}
345
+ />
346
+
347
+ {/* Selection indicator */}
348
+ {hasSelection && (
349
+ <mesh rotation={[-Math.PI / 2, 0, 0]}>
350
+ <ringGeometry args={[orbitRadius - 0.12, orbitRadius + 0.12, 64]} />
351
+ <meshBasicMaterial
352
+ color="#ffffff"
353
+ transparent
354
+ opacity={0.3}
355
+ side={THREE.DoubleSide}
356
+ />
357
+ </mesh>
358
+ )}
359
+
360
+ {/* Hover tooltip */}
361
+ {isHovered && (
362
+ <group position={[0, 0.3, 0]}>
363
+ <StatusTooltip counts={statusCounts} total={receipts.length} />
364
+ </group>
365
+ )}
366
+ </group>
367
+ );
368
+ }