@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,370 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CardinalMenu
5
+ *
6
+ * The main menu that appears around the Sentinel orb with items
7
+ * positioned along a 180-degree arc (top half only, from left to right).
8
+ */
9
+
10
+ import { AnimatePresence, motion } from 'framer-motion';
11
+ import React, { useCallback, useMemo, useState } from 'react';
12
+ import styled from 'styled-components';
13
+
14
+ import type { CardinalMenuProps, DocketItem } from './types';
15
+ import { useSentinelStore } from './sentinelStore';
16
+ import { useSentinelDependencies } from './SentinelProvider';
17
+ import { CardinalItem } from './CardinalItem';
18
+ import { RadialSubmenu } from './RadialSubmenu';
19
+ import { VerticalSubmenu } from './VerticalSubmenu';
20
+
21
+ // -----------------------------------------------------------------------------
22
+ // Constants
23
+ // -----------------------------------------------------------------------------
24
+
25
+ const ARC_RADIUS = 120;
26
+ // Arc spans from 180 degrees (left) to 0 degrees (right) - top half only
27
+ const ARC_START_ANGLE = 180; // degrees - left side
28
+ const ARC_END_ANGLE = 0; // degrees - right side
29
+
30
+ /**
31
+ * Calculate position for item at index i out of n items across the top 180-degree arc.
32
+ * Arc goes from 180 degrees (left) to 0 degrees (right), distributed evenly.
33
+ */
34
+ function calculateArcPosition(
35
+ index: number,
36
+ total: number,
37
+ radius: number = ARC_RADIUS
38
+ ): { x: number; y: number } {
39
+ // For single item, place at top (90 degrees)
40
+ // For multiple items, distribute evenly from 180 to 0 degrees
41
+ let angleDegrees: number;
42
+
43
+ if (total === 1) {
44
+ angleDegrees = 90; // top center
45
+ } else {
46
+ // Distribute from 180 to 0 degrees (left to right across top)
47
+ angleDegrees = ARC_START_ANGLE - (ARC_START_ANGLE - ARC_END_ANGLE) * (index / (total - 1));
48
+ }
49
+
50
+ const radians = (angleDegrees * Math.PI) / 180;
51
+
52
+ return {
53
+ x: Math.cos(radians) * radius,
54
+ y: -Math.sin(radians) * radius, // negative because screen Y is inverted
55
+ };
56
+ }
57
+
58
+ // -----------------------------------------------------------------------------
59
+ // Styled Components
60
+ // -----------------------------------------------------------------------------
61
+
62
+ const MenuContainer = styled(motion.div)`
63
+ position: fixed;
64
+ width: 0;
65
+ height: 0;
66
+ top: 50%;
67
+ left: 50%;
68
+ transform: translate(-50%, -50%);
69
+ z-index: 9600;
70
+ pointer-events: none;
71
+ `;
72
+
73
+ const ItemWrapper = styled(motion.div)`
74
+ position: absolute;
75
+ pointer-events: auto;
76
+ transform-origin: center;
77
+ `;
78
+
79
+ const ItemPositioner = styled.div`
80
+ transform: translate(-50%, -50%);
81
+ `;
82
+
83
+ const EditLink = styled(motion.a)`
84
+ position: absolute;
85
+ left: 50%;
86
+ transform: translateX(-50%);
87
+ top: 32px;
88
+ font-family: var(--font-mono);
89
+ font-size: 10px;
90
+ text-transform: uppercase;
91
+ letter-spacing: 0.1em;
92
+ color: rgba(212, 168, 75, 0.5);
93
+ cursor: pointer;
94
+ text-decoration: none;
95
+ transition: color 0.2s ease;
96
+ pointer-events: auto;
97
+
98
+ &:hover {
99
+ color: rgba(212, 168, 75, 1);
100
+ }
101
+ `;
102
+
103
+ // -----------------------------------------------------------------------------
104
+ // Animation Variants
105
+ // -----------------------------------------------------------------------------
106
+
107
+ const containerVariants = {
108
+ hidden: {
109
+ opacity: 0,
110
+ },
111
+ visible: {
112
+ opacity: 1,
113
+ transition: {
114
+ staggerChildren: 0.08,
115
+ delayChildren: 0.1,
116
+ },
117
+ },
118
+ exit: {
119
+ opacity: 0,
120
+ transition: {
121
+ staggerChildren: 0.04,
122
+ staggerDirection: -1,
123
+ },
124
+ },
125
+ };
126
+
127
+ const itemVariants = {
128
+ hidden: {
129
+ opacity: 0,
130
+ scale: 0.6,
131
+ },
132
+ visible: {
133
+ opacity: 1,
134
+ scale: 1,
135
+ transition: {
136
+ type: 'spring' as const,
137
+ stiffness: 300,
138
+ damping: 24,
139
+ },
140
+ },
141
+ exit: {
142
+ opacity: 0,
143
+ scale: 0.6,
144
+ transition: {
145
+ duration: 0.15,
146
+ },
147
+ },
148
+ };
149
+
150
+ // -----------------------------------------------------------------------------
151
+ // Component
152
+ // -----------------------------------------------------------------------------
153
+
154
+ export const CardinalMenu: React.FC<CardinalMenuProps> = ({
155
+ items: itemsProp,
156
+ onEdit,
157
+ }) => {
158
+ const phase = useSentinelStore((s) => s.phase);
159
+ const docket = useSentinelStore((s) => s.docket);
160
+ const dismiss = useSentinelStore((s) => s.dismiss);
161
+ const enterAvatarMode = useSentinelStore((s) => s.enterAvatarMode);
162
+ const expandedCardinal = useSentinelStore((s) => s.expandedCardinal);
163
+ const expandCardinal = useSentinelStore((s) => s.expandCardinal);
164
+ const orbPosition = useSentinelStore((s) => s.orbPosition);
165
+
166
+ const { onOpenProcess, processMap } = useSentinelDependencies();
167
+
168
+ // Local state for radial hover (for vertical submenu)
169
+ const [hoveredRadialId, setHoveredRadialId] = useState<string | null>(null);
170
+ const [hoveredRadialItem, setHoveredRadialItem] = useState<DocketItem | null>(null);
171
+ const [radialItemPosition, setRadialItemPosition] = useState<{ x: number; y: number } | null>(null);
172
+ // Track which item index is expanded (for arc-based items)
173
+ const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
174
+
175
+ const isVisible = phase === 'open';
176
+
177
+ // Use provided items or fall back to docket items with position
178
+ const arcItems = useMemo(() => {
179
+ if (itemsProp && itemsProp.length > 0) {
180
+ return itemsProp;
181
+ }
182
+ // Fallback: use docket items that have a cardinal position
183
+ return docket.filter((item) => item.position !== undefined);
184
+ }, [itemsProp, docket]);
185
+
186
+ // Get expanded item (if any) - either by index (new system) or cardinal (legacy)
187
+ const expandedItem = useMemo(() => {
188
+ if (expandedIndex !== null && arcItems[expandedIndex]) {
189
+ return arcItems[expandedIndex];
190
+ }
191
+ if (expandedCardinal) {
192
+ return arcItems.find((item) => item.position === expandedCardinal) || null;
193
+ }
194
+ return null;
195
+ }, [expandedIndex, expandedCardinal, arcItems]);
196
+
197
+ const handleItemClick = useCallback(
198
+ (item: DocketItem, index: number) => {
199
+ // If item has children, toggle expansion
200
+ if (item.children && item.children.length > 0) {
201
+ // Check if this item is expanded (by index or cardinal)
202
+ const isCurrentlyExpanded =
203
+ expandedIndex === index ||
204
+ (item.position && expandedCardinal === item.position);
205
+
206
+ if (isCurrentlyExpanded) {
207
+ setExpandedIndex(null);
208
+ expandCardinal(null); // Collapse
209
+ } else {
210
+ setExpandedIndex(index);
211
+ if (item.position) {
212
+ expandCardinal(item.position); // For legacy support
213
+ }
214
+ }
215
+ return;
216
+ }
217
+
218
+ // Leaf action
219
+ if (item.id === 'avatar') {
220
+ enterAvatarMode();
221
+ } else {
222
+ const processId = processMap?.[item.id];
223
+ if (processId && onOpenProcess) {
224
+ onOpenProcess(processId, {});
225
+ }
226
+ dismiss();
227
+ }
228
+ },
229
+ [expandedIndex, expandedCardinal, expandCardinal, enterAvatarMode, onOpenProcess, processMap, dismiss]
230
+ );
231
+
232
+ const handleRadialItemClick = useCallback(
233
+ (radialItem: DocketItem) => {
234
+ // If item has children, we show vertical submenu on hover
235
+ // On click, just execute action if leaf
236
+ if (!radialItem.children || radialItem.children.length === 0) {
237
+ if (radialItem.action) {
238
+ radialItem.action();
239
+ }
240
+ dismiss();
241
+ }
242
+ },
243
+ [dismiss]
244
+ );
245
+
246
+ const handleRadialItemHover = useCallback(
247
+ (itemId: string | null) => {
248
+ setHoveredRadialId(itemId);
249
+ if (itemId && expandedItem?.children) {
250
+ const item = expandedItem.children.find((c) => c.id === itemId);
251
+ setHoveredRadialItem(item || null);
252
+ // Calculate position based on orb center + item position on arc
253
+ if (item && expandedIndex !== null) {
254
+ const arcPos = calculateArcPosition(expandedIndex, arcItems.length);
255
+ setRadialItemPosition({
256
+ x: orbPosition.x + arcPos.x,
257
+ y: orbPosition.y + arcPos.y,
258
+ });
259
+ } else if (item && expandedItem.position) {
260
+ // Legacy cardinal fallback
261
+ const cardinalIndex = arcItems.findIndex((i) => i.id === expandedItem.id);
262
+ if (cardinalIndex >= 0) {
263
+ const arcPos = calculateArcPosition(cardinalIndex, arcItems.length);
264
+ setRadialItemPosition({
265
+ x: orbPosition.x + arcPos.x,
266
+ y: orbPosition.y + arcPos.y,
267
+ });
268
+ }
269
+ }
270
+ } else {
271
+ setHoveredRadialItem(null);
272
+ setRadialItemPosition(null);
273
+ }
274
+ },
275
+ [expandedItem, expandedIndex, arcItems, orbPosition]
276
+ );
277
+
278
+ const handleVerticalItemClick = useCallback(
279
+ (verticalItem: DocketItem) => {
280
+ if (verticalItem.action) {
281
+ verticalItem.action();
282
+ }
283
+ dismiss();
284
+ },
285
+ [dismiss]
286
+ );
287
+
288
+ return (
289
+ <AnimatePresence>
290
+ {isVisible && (
291
+ <MenuContainer
292
+ key="cardinal-menu"
293
+ variants={containerVariants}
294
+ initial="hidden"
295
+ animate="visible"
296
+ exit="exit"
297
+ style={{ left: orbPosition.x, top: orbPosition.y }}
298
+ >
299
+ {arcItems.map((item, index) => {
300
+ const offset = calculateArcPosition(index, arcItems.length);
301
+ const isExpanded =
302
+ expandedIndex === index ||
303
+ (item.position && expandedCardinal === item.position);
304
+
305
+ return (
306
+ <ItemWrapper
307
+ key={item.id}
308
+ variants={itemVariants}
309
+ style={{
310
+ left: offset.x,
311
+ top: offset.y,
312
+ }}
313
+ >
314
+ <ItemPositioner>
315
+ <CardinalItem
316
+ icon={item.icon}
317
+ label={item.label}
318
+ position={item.position || 'N'}
319
+ onClick={() => handleItemClick(item, index)}
320
+ />
321
+
322
+ {/* Radial submenu for expanded item */}
323
+ {isExpanded && item.children && item.children.length > 0 && (
324
+ <RadialSubmenu
325
+ items={item.children}
326
+ centerOffset={{ x: 0, y: 0 }}
327
+ arcConstraint="semicircle"
328
+ onItemClick={handleRadialItemClick}
329
+ onItemHover={handleRadialItemHover}
330
+ hoveredItemId={hoveredRadialId}
331
+ />
332
+ )}
333
+ </ItemPositioner>
334
+ </ItemWrapper>
335
+ );
336
+ })}
337
+
338
+ {/* Edit link below arc center */}
339
+ {onEdit && (
340
+ <EditLink
341
+ initial={{ opacity: 0 }}
342
+ animate={{ opacity: 1 }}
343
+ exit={{ opacity: 0 }}
344
+ transition={{ delay: 0.3, duration: 0.2 }}
345
+ onClick={(e: React.MouseEvent) => {
346
+ e.preventDefault();
347
+ onEdit();
348
+ }}
349
+ role="button"
350
+ tabIndex={0}
351
+ >
352
+ edit
353
+ </EditLink>
354
+ )}
355
+
356
+ {/* Vertical submenu for hovered radial item */}
357
+ {hoveredRadialItem?.children && hoveredRadialItem.children.length > 0 && radialItemPosition && (
358
+ <VerticalSubmenu
359
+ items={hoveredRadialItem.children}
360
+ anchorPosition={radialItemPosition}
361
+ onItemClick={handleVerticalItemClick}
362
+ />
363
+ )}
364
+ </MenuContainer>
365
+ )}
366
+ </AnimatePresence>
367
+ );
368
+ };
369
+
370
+ export default CardinalMenu;
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * DockedMiniOrb
5
+ *
6
+ * A small orb that appears at the edge of the screen when Sentinel is docked
7
+ * (not in the taskbar). Clicking it summons the full Sentinel.
8
+ */
9
+
10
+ import { Center } from '@react-three/drei';
11
+ import { Canvas } from '@react-three/fiber';
12
+ import { motion } from 'framer-motion';
13
+ import React, { Suspense, useCallback, useEffect, useState } from 'react';
14
+ import styled, { keyframes } from 'styled-components';
15
+
16
+ import { useSentinelStore } from './sentinelStore';
17
+ import { useSentinelDependencies } from './SentinelProvider';
18
+ import { SigilPlaceholder } from './SigilPlaceholder';
19
+
20
+ const ORB_SIZE = 32;
21
+ const EDGE_OFFSET = 16;
22
+
23
+ const subtlePulse = keyframes`
24
+ 0%, 100% {
25
+ box-shadow: 0 2px 12px rgba(212, 168, 75, 0.3),
26
+ 0 0 16px rgba(212, 168, 75, 0.15);
27
+ }
28
+ 50% {
29
+ box-shadow: 0 2px 16px rgba(212, 168, 75, 0.45),
30
+ 0 0 24px rgba(212, 168, 75, 0.25);
31
+ }
32
+ `;
33
+
34
+ const OrbContainer = styled(motion.div)`
35
+ position: fixed;
36
+ z-index: 9999;
37
+ width: ${ORB_SIZE}px;
38
+ height: ${ORB_SIZE}px;
39
+ border-radius: 50%;
40
+ border: 1px solid rgba(212, 168, 75, 0.4);
41
+ background: linear-gradient(160deg, rgba(0, 0, 0, 0.65), rgba(8, 8, 10, 0.9));
42
+ overflow: hidden;
43
+ cursor: pointer;
44
+ animation: ${subtlePulse} 3s ease-in-out infinite;
45
+ `;
46
+
47
+ export const DockedMiniOrb: React.FC = () => {
48
+ const phase = useSentinelStore((s) => s.phase);
49
+ const dockedPosition = useSentinelStore((s) => s.dockedPosition);
50
+ const summon = useSentinelStore((s) => s.summon);
51
+ const setTaskbarOrigin = useSentinelStore((s) => s.setTaskbarOrigin);
52
+ const setOrbPosition = useSentinelStore((s) => s.setOrbPosition);
53
+
54
+ const { GlyphRenderer } = useSentinelDependencies();
55
+
56
+ const [position, setPosition] = useState({ left: 0, top: 0 });
57
+
58
+ // Calculate position based on dockedPosition
59
+ useEffect(() => {
60
+ if (dockedPosition === 'taskbar') return;
61
+
62
+ const calculatePosition = () => {
63
+ const { edge, percent } = dockedPosition;
64
+
65
+ let left = 0;
66
+ let top = 0;
67
+
68
+ const width = typeof window !== 'undefined' ? window.innerWidth : 800;
69
+ const height = typeof window !== 'undefined' ? window.innerHeight : 600;
70
+
71
+ switch (edge) {
72
+ case 'top':
73
+ left = percent * width;
74
+ top = EDGE_OFFSET;
75
+ break;
76
+ case 'left':
77
+ left = EDGE_OFFSET;
78
+ top = percent * height;
79
+ break;
80
+ case 'right':
81
+ left = width - EDGE_OFFSET;
82
+ top = percent * height;
83
+ break;
84
+ }
85
+
86
+ setPosition({ left, top });
87
+ };
88
+
89
+ calculatePosition();
90
+
91
+ if (typeof window !== 'undefined') {
92
+ window.addEventListener('resize', calculatePosition);
93
+ return () => window.removeEventListener('resize', calculatePosition);
94
+ }
95
+ }, [dockedPosition]);
96
+
97
+ const handleClick = useCallback(() => {
98
+ // Set taskbar origin to current position before summoning
99
+ const origin = { x: position.left, y: position.top };
100
+ setTaskbarOrigin(origin);
101
+ setOrbPosition(origin);
102
+ summon();
103
+ }, [summon, setTaskbarOrigin, setOrbPosition, position]);
104
+
105
+ // Only render when docked at an edge (not taskbar)
106
+ if (phase !== 'docked' || dockedPosition === 'taskbar') {
107
+ return null;
108
+ }
109
+
110
+ return (
111
+ <OrbContainer
112
+ style={{
113
+ left: position.left - ORB_SIZE / 2,
114
+ top: position.top - ORB_SIZE / 2,
115
+ }}
116
+ onClick={handleClick}
117
+ whileHover={{ scale: 1.12 }}
118
+ whileTap={{ scale: 0.95 }}
119
+ transition={{ type: 'spring', stiffness: 400, damping: 20 }}
120
+ >
121
+ <Canvas
122
+ camera={{ position: [0, 0, 4], fov: 45 }}
123
+ gl={{ alpha: true, antialias: true }}
124
+ style={{ width: '100%', height: '100%' }}
125
+ >
126
+ <ambientLight intensity={0.4} />
127
+ <directionalLight position={[2, 2, 3]} intensity={0.8} />
128
+ <Suspense
129
+ fallback={
130
+ <Center>
131
+ <SigilPlaceholder scale={0.85} />
132
+ </Center>
133
+ }
134
+ >
135
+ {GlyphRenderer && (
136
+ <Center>
137
+ <GlyphRenderer variant="sentinel" scale={0.7} />
138
+ </Center>
139
+ )}
140
+ </Suspense>
141
+ </Canvas>
142
+ </OrbContainer>
143
+ );
144
+ };
145
+
146
+ export default DockedMiniOrb;