@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.
- package/package.json +43 -0
- package/src/environment/AuroraLayer/AuroraLayer.stories.tsx +43 -0
- package/src/environment/AuroraLayer/AuroraLayer.tsx +200 -0
- package/src/environment/AuroraLayer/index.ts +2 -0
- package/src/environment/AuroraLayer/types.ts +30 -0
- package/src/environment/EnvironmentLayer/EnvironmentLayer.stories.tsx +262 -0
- package/src/environment/EnvironmentLayer/EnvironmentLayer.tsx +105 -0
- package/src/environment/EnvironmentLayer/index.ts +4 -0
- package/src/environment/EnvironmentLayer/presets.ts +128 -0
- package/src/environment/FogLayer/FogLayer.stories.tsx +83 -0
- package/src/environment/FogLayer/FogLayer.tsx +113 -0
- package/src/environment/FogLayer/index.ts +2 -0
- package/src/environment/FogLayer/types.ts +45 -0
- package/src/environment/VolumetricLight/VolumetricLight.stories.tsx +55 -0
- package/src/environment/VolumetricLight/VolumetricLight.tsx +188 -0
- package/src/environment/VolumetricLight/index.ts +2 -0
- package/src/environment/VolumetricLight/types.ts +33 -0
- package/src/environment/WeatherLayer/WeatherLayer.stories.tsx +348 -0
- package/src/environment/WeatherLayer/WeatherLayer.tsx +266 -0
- package/src/environment/WeatherLayer/cinematicCanvas.tsx +809 -0
- package/src/environment/WeatherLayer/colors.ts +27 -0
- package/src/environment/WeatherLayer/index.ts +4 -0
- package/src/environment/WeatherLayer/leafPresets.ts +12 -0
- package/src/environment/WeatherLayer/particles.ts +227 -0
- package/src/environment/WeatherLayer/types.ts +140 -0
- package/src/environment/index.ts +17 -0
- package/src/environment/shared/index.ts +2 -0
- package/src/environment/shared/noise.ts +10 -0
- package/src/environment/shared/performance.ts +33 -0
- package/src/environment/shared/types.ts +26 -0
- package/src/index.ts +2 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vision-types.ts +84 -0
- package/src/three/AgentConsole/AgentConsole.stories.tsx +397 -0
- package/src/three/AgentConsole/AgentConsole.tsx +195 -0
- package/src/three/AgentConsole/ConsoleChat.tsx +237 -0
- package/src/three/AgentConsole/FocusConstellation.tsx +297 -0
- package/src/three/AgentConsole/GlyphAvatar.stories.tsx +110 -0
- package/src/three/AgentConsole/GlyphAvatar.tsx +117 -0
- package/src/three/AgentConsole/QuickActions.tsx +111 -0
- package/src/three/AgentConsole/index.ts +41 -0
- package/src/three/AgentConsole/types.ts +241 -0
- package/src/three/AmbientField/AmbientField.stories.tsx +290 -0
- package/src/three/AmbientField/AmbientField.tsx +307 -0
- package/src/three/AmbientField/BackbayFieldBus.ts +326 -0
- package/src/three/AmbientField/FieldProvider.tsx +83 -0
- package/src/three/AmbientField/index.ts +37 -0
- package/src/three/AmbientField/shaders/constellation.ts +384 -0
- package/src/three/AmbientField/types.ts +174 -0
- package/src/three/AttackGraph/AttackGraph.stories.tsx +144 -0
- package/src/three/AttackGraph/AttackGraph.tsx +325 -0
- package/src/three/AttackGraph/index.ts +19 -0
- package/src/three/AttackGraph/types.ts +97 -0
- package/src/three/AuditTrail/AuditTrail.stories.tsx +567 -0
- package/src/three/AuditTrail/AuditTrail.tsx +644 -0
- package/src/three/AuditTrail/index.ts +33 -0
- package/src/three/AuditTrail/types.ts +192 -0
- package/src/three/CrystallineOrganism/Breadcrumb.tsx +61 -0
- package/src/three/CrystallineOrganism/CrystallineOrganism.stories.tsx +509 -0
- package/src/three/CrystallineOrganism/CrystallineOrganism.tsx +273 -0
- package/src/three/CrystallineOrganism/LatticeEdge.tsx +69 -0
- package/src/three/CrystallineOrganism/OrganismLattice.tsx +159 -0
- package/src/three/CrystallineOrganism/OrganismParticles.tsx +148 -0
- package/src/three/CrystallineOrganism/OrganismShell.tsx +277 -0
- package/src/three/CrystallineOrganism/constants.ts +161 -0
- package/src/three/CrystallineOrganism/index.ts +17 -0
- package/src/three/CrystallineOrganism/layouts/hexGrid.ts +85 -0
- package/src/three/CrystallineOrganism/layouts/index.ts +1 -0
- package/src/three/CrystallineOrganism/types.ts +167 -0
- package/src/three/CrystallineOrganism/useOrganismEmotion.ts +84 -0
- package/src/three/FirewallBarrier/FirewallBarrier.stories.tsx +167 -0
- package/src/three/FirewallBarrier/FirewallBarrier.tsx +259 -0
- package/src/three/FirewallBarrier/index.ts +14 -0
- package/src/three/FirewallBarrier/types.ts +52 -0
- package/src/three/Glyph/GlyphObject.stories.tsx +577 -0
- package/src/three/Glyph/GlyphObject.tsx +422 -0
- package/src/three/Glyph/index.ts +10 -0
- package/src/three/Glyph/types.ts +36 -0
- package/src/three/Glyph/useGlyphController.ts +231 -0
- package/src/three/Glyph/useGlyphEmotion.ts +70 -0
- package/src/three/Graph3D/Graph3D.stories.tsx +269 -0
- package/src/three/Graph3D/Graph3D.tsx +248 -0
- package/src/three/Graph3D/GraphEdge.tsx +79 -0
- package/src/three/Graph3D/GraphNode.tsx +239 -0
- package/src/three/Graph3D/types.ts +66 -0
- package/src/three/Graph3D/utils.ts +204 -0
- package/src/three/IntelFeed/IntelFeed.stories.tsx +168 -0
- package/src/three/IntelFeed/IntelFeed.tsx +284 -0
- package/src/three/IntelFeed/index.ts +14 -0
- package/src/three/IntelFeed/types.ts +56 -0
- package/src/three/MetricsGalaxy/MetricsGalaxy.tsx +484 -0
- package/src/three/MetricsGalaxy/index.ts +6 -0
- package/src/three/MetricsGalaxy/types.ts +26 -0
- package/src/three/NetworkTopology/NetworkTopology.stories.tsx +184 -0
- package/src/three/NetworkTopology/NetworkTopology.tsx +421 -0
- package/src/three/NetworkTopology/index.ts +34 -0
- package/src/three/NetworkTopology/types.ts +128 -0
- package/src/three/ParticleField/ParticleField.stories.tsx +162 -0
- package/src/three/ParticleField/ParticleField.tsx +81 -0
- package/src/three/ParticleField/index.ts +1 -0
- package/src/three/PermissionMatrix/PermissionMatrix.stories.tsx +475 -0
- package/src/three/PermissionMatrix/PermissionMatrix.tsx +380 -0
- package/src/three/PermissionMatrix/index.ts +15 -0
- package/src/three/PermissionMatrix/types.ts +54 -0
- package/src/three/QuantumField/ConstellationField.tsx +238 -0
- package/src/three/QuantumField/FieldBus.ts +349 -0
- package/src/three/QuantumField/FieldLayer.tsx +430 -0
- package/src/three/QuantumField/FieldProvider.tsx +460 -0
- package/src/three/QuantumField/PcbField.tsx +406 -0
- package/src/three/QuantumField/QuantumField.stories.tsx +1155 -0
- package/src/three/QuantumField/TrailRTT.ts +212 -0
- package/src/three/QuantumField/WaterField.tsx +226 -0
- package/src/three/QuantumField/WaterSimRTT.ts +283 -0
- package/src/three/QuantumField/domMapping.ts +185 -0
- package/src/three/QuantumField/index.ts +110 -0
- package/src/three/QuantumField/styles/index.ts +9 -0
- package/src/three/QuantumField/styles/styleA.ts +526 -0
- package/src/three/QuantumField/styles/styleB.ts +1210 -0
- package/src/three/QuantumField/styles/styleC.ts +266 -0
- package/src/three/QuantumField/themes.ts +211 -0
- package/src/three/QuantumField/types.ts +380 -0
- package/src/three/SOCCommandCenter/SOCCommandCenter.stories.tsx +591 -0
- package/src/three/SOCCommandCenter/SOCCommandCenter.tsx +248 -0
- package/src/three/SOCCommandCenter/index.ts +26 -0
- package/src/three/SOCCommandCenter/types.ts +201 -0
- package/src/three/SecurityDashboard/SecurityDashboard.stories.tsx +508 -0
- package/src/three/SecurityDashboard/SecurityDashboard.tsx +507 -0
- package/src/three/SecurityDashboard/index.ts +37 -0
- package/src/three/SecurityDashboard/types.ts +143 -0
- package/src/three/SecurityShield/SecurityShield.stories.tsx +257 -0
- package/src/three/SecurityShield/SecurityShield.tsx +502 -0
- package/src/three/SecurityShield/index.ts +25 -0
- package/src/three/SecurityShield/types.ts +64 -0
- package/src/three/Sentinel/AvatarMode.tsx +578 -0
- package/src/three/Sentinel/AvatarRenderer.tsx +199 -0
- package/src/three/Sentinel/CameraPip.tsx +127 -0
- package/src/three/Sentinel/CardinalItem.tsx +83 -0
- package/src/three/Sentinel/CardinalMenu.tsx +370 -0
- package/src/three/Sentinel/DockedMiniOrb.tsx +146 -0
- package/src/three/Sentinel/RadialSubmenu.tsx +273 -0
- package/src/three/Sentinel/SentinelConversation.tsx +802 -0
- package/src/three/Sentinel/SentinelOrb.tsx +316 -0
- package/src/three/Sentinel/SentinelOverlay.tsx +146 -0
- package/src/three/Sentinel/SentinelProvider.tsx +145 -0
- package/src/three/Sentinel/SentinelTether.tsx +182 -0
- package/src/three/Sentinel/SigilPlaceholder.tsx +176 -0
- package/src/three/Sentinel/VerticalSubmenu.tsx +150 -0
- package/src/three/Sentinel/index.ts +145 -0
- package/src/three/Sentinel/sentinelStore.ts +196 -0
- package/src/three/Sentinel/types.ts +403 -0
- package/src/three/Sentinel/useCameraPermission.ts +153 -0
- package/src/three/Sentinel/useThrowPhysics.ts +220 -0
- package/src/three/SpatialWorkspace/CyntraWorkspace.tsx +84 -0
- package/src/three/SpatialWorkspace/JobCluster.tsx +281 -0
- package/src/three/SpatialWorkspace/NodeGraph.tsx +236 -0
- package/src/three/SpatialWorkspace/ReceiptOrbit.tsx +368 -0
- package/src/three/SpatialWorkspace/SpatialWorkspace.stories.tsx +547 -0
- package/src/three/SpatialWorkspace/SpatialWorkspace.tsx +428 -0
- package/src/three/SpatialWorkspace/TrustRings.tsx +228 -0
- package/src/three/SpatialWorkspace/adapters.ts +353 -0
- package/src/three/SpatialWorkspace/index.ts +85 -0
- package/src/three/SpatialWorkspace/nexusAdapter.ts +182 -0
- package/src/three/SpatialWorkspace/types.ts +389 -0
- package/src/three/ThreatRadar/ThreatRadar.stories.tsx +451 -0
- package/src/three/ThreatRadar/ThreatRadar.tsx +542 -0
- package/src/three/ThreatRadar/index.ts +8 -0
- package/src/three/ThreatRadar/types.ts +90 -0
- package/src/three/ThreeErrorBoundary/ThreeErrorBoundary.tsx +235 -0
- package/src/three/ThreeErrorBoundary/index.ts +5 -0
- package/src/three/index.ts +56 -0
- package/tsconfig.json +20 -0
- 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;
|