@ibgib/space-gib 0.0.33 → 0.0.35

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 (118) hide show
  1. package/dist/client/bootstrap.mjs +24 -51
  2. package/dist/client/chunk-FIAGRVBY.mjs +10933 -0
  3. package/dist/client/chunk-XLQ3UCQY.mjs +53 -0
  4. package/dist/client/css/activity-bar.css +435 -0
  5. package/dist/client/css/base.css +167 -0
  6. package/dist/client/css/components.css +218 -0
  7. package/dist/client/css/cosmic.css +557 -0
  8. package/dist/client/css/dev-tools.css +163 -0
  9. package/dist/client/css/landing.css +184 -0
  10. package/dist/client/css/layout.css +1017 -0
  11. package/dist/client/css/onboarding.css +181 -0
  12. package/dist/client/css/repos.css +740 -0
  13. package/dist/client/css/variables.css +64 -0
  14. package/dist/client/index.html +208 -31
  15. package/dist/client/index.mjs +22 -45
  16. package/dist/client/privacy.html +22 -22
  17. package/dist/client/script.mjs +1 -1
  18. package/dist/client/style.css +11 -1056
  19. package/dist/respec-gib.node.mjs +5 -0
  20. package/dist/server/server.mjs +6880 -3110
  21. package/package.json +6 -6
  22. package/src/client/AUTO-GENERATED-version.mts +1 -1
  23. package/src/client/cosmos/seed-cosmos-workspace.mts +83 -0
  24. package/src/client/css/activity-bar.css +435 -0
  25. package/src/client/css/base.css +167 -0
  26. package/src/client/css/components.css +218 -0
  27. package/src/client/css/cosmic.css +557 -0
  28. package/src/client/css/dev-tools.css +163 -0
  29. package/src/client/css/landing.css +184 -0
  30. package/src/client/css/layout.css +1017 -0
  31. package/src/client/css/onboarding.css +181 -0
  32. package/src/client/css/repos.css +740 -0
  33. package/src/client/css/variables.css +64 -0
  34. package/src/client/dev-tools/vcs-workspace.mts +7 -7
  35. package/src/client/index.html +208 -31
  36. package/src/client/privacy.html +22 -22
  37. package/src/client/style.css +11 -1056
  38. package/src/client/ui/component/changes/changes.css +413 -0
  39. package/src/client/ui/component/changes/changes.html +37 -0
  40. package/src/client/ui/component/changes/changes.mts +766 -0
  41. package/src/client/ui/component/changes/index.mts +9 -0
  42. package/src/client/ui/component/chat-view/IMPLEMENTATION.md +48 -0
  43. package/src/client/ui/component/chat-view/chat-view.css +381 -0
  44. package/src/client/ui/component/chat-view/chat-view.html +52 -0
  45. package/src/client/ui/component/chat-view/chat-view.mts +264 -0
  46. package/src/client/ui/component/chat-view/index.mts +9 -0
  47. package/src/client/ui/component/clone/IMPLEMENTATION.md +68 -0
  48. package/src/client/ui/component/clone/clone-constants.mts +16 -0
  49. package/src/client/ui/component/clone/clone-types.mts +60 -0
  50. package/src/client/ui/component/clone/clone.css +383 -0
  51. package/src/client/ui/component/clone/clone.html +89 -0
  52. package/src/client/ui/component/clone/clone.mts +648 -0
  53. package/src/client/ui/component/clone/index.mts +11 -0
  54. package/src/client/ui/component/code-editor/IMPLEMENTATION.md +89 -0
  55. package/src/client/ui/component/code-editor/code-editor.css +357 -0
  56. package/src/client/ui/component/code-editor/code-editor.html +67 -0
  57. package/src/client/ui/component/code-editor/code-editor.mts +643 -0
  58. package/src/client/ui/component/code-editor/index.mts +9 -0
  59. package/src/client/ui/component/cosmos-navigator/IMPLEMENTATION.md +31 -0
  60. package/src/client/ui/component/cosmos-navigator/cosmos-navigator.css +864 -0
  61. package/src/client/ui/component/cosmos-navigator/cosmos-navigator.html +92 -0
  62. package/src/client/ui/component/cosmos-navigator/cosmos-navigator.mts +1317 -0
  63. package/src/client/ui/component/cosmos-navigator/index.mts +18 -0
  64. package/src/client/ui/component/file-explorer/IMPLEMENTATION.md +42 -0
  65. package/src/client/ui/component/file-explorer/file-explorer.css +720 -0
  66. package/src/client/ui/component/file-explorer/file-explorer.html +55 -0
  67. package/src/client/ui/component/file-explorer/file-explorer.mts +1412 -0
  68. package/src/client/ui/component/file-explorer/index.mts +9 -0
  69. package/src/client/ui/component/nested-chat/IMPLEMENTATION.md +18 -0
  70. package/src/client/ui/component/nested-chat/chat/IMPLEMENTATION.md +14 -0
  71. package/src/client/ui/component/nested-chat/chat/chat-constants.mts +14 -0
  72. package/src/client/ui/component/nested-chat/chat/chat-helpers.mts +66 -0
  73. package/src/client/ui/component/nested-chat/chat/chat.css +654 -0
  74. package/src/client/ui/component/nested-chat/chat/chat.html +90 -0
  75. package/src/client/ui/component/nested-chat/chat/chat.mts +561 -0
  76. package/src/client/ui/component/nested-chat/chat/index.mts +7 -0
  77. package/src/client/ui/component/nested-chat/index.mts +14 -0
  78. package/src/client/ui/component/nested-chat/nested-chat-constants.mts +12 -0
  79. package/src/client/ui/component/nested-chat/nested-chat-types.mts +53 -0
  80. package/src/client/ui/component/nested-chat/nested-chat.css +142 -0
  81. package/src/client/ui/component/nested-chat/nested-chat.html +11 -0
  82. package/src/client/ui/component/nested-chat/nested-chat.mts +395 -0
  83. package/src/client/ui/component/notes-explorer/index.mts +9 -0
  84. package/src/client/ui/component/notes-explorer/notes-explorer.css +313 -0
  85. package/src/client/ui/component/notes-explorer/notes-explorer.html +136 -0
  86. package/src/client/ui/component/notes-explorer/notes-explorer.mts +240 -0
  87. package/src/client/ui/component/notes-inspector/index.mts +9 -0
  88. package/src/client/ui/component/notes-inspector/notes-inspector.css +244 -0
  89. package/src/client/ui/component/notes-inspector/notes-inspector.html +97 -0
  90. package/src/client/ui/component/notes-inspector/notes-inspector.mts +152 -0
  91. package/src/client/ui/component/pitch/IMPLEMENTATION.md +48 -0
  92. package/src/client/ui/component/pitch/index.mts +9 -0
  93. package/src/client/ui/component/pitch/pitch.css +295 -0
  94. package/src/client/ui/component/pitch/pitch.html +97 -0
  95. package/src/client/ui/component/pitch/pitch.mts +135 -0
  96. package/src/client/ui/component/timeline/IMPLEMENTATION.md +33 -0
  97. package/src/client/ui/component/timeline/index.mts +9 -0
  98. package/src/client/ui/component/timeline/timeline.css +279 -0
  99. package/src/client/ui/component/timeline/timeline.html +23 -0
  100. package/src/client/ui/component/timeline/timeline.mts +261 -0
  101. package/src/client/ui/component/timeline-item-details/IMPLEMENTATION.md +28 -0
  102. package/src/client/ui/component/timeline-item-details/index.mts +9 -0
  103. package/src/client/ui/component/timeline-item-details/timeline-item-details.css +286 -0
  104. package/src/client/ui/component/timeline-item-details/timeline-item-details.html +64 -0
  105. package/src/client/ui/component/timeline-item-details/timeline-item-details.mts +194 -0
  106. package/src/client/ui/router/index.mts +3 -0
  107. package/src/client/ui/router/space-gib-router-helpers.mts +72 -0
  108. package/src/client/ui/router/space-gib-router-helpers.respec.mts +529 -0
  109. package/src/client/ui/router/space-gib-router-service.mts +63 -0
  110. package/src/client/ui/router/space-gib-router-types.mts +49 -0
  111. package/src/client/ui/shell/cosmic-big-bang.mts +853 -0
  112. package/src/client/ui/shell/space-gib-shell-constants.mts +7 -0
  113. package/src/client/ui/shell/space-gib-shell-service.mts +2297 -32
  114. package/src/server/server.mts +20 -1
  115. package/tsconfig.test.json +5 -1
  116. package/dist/client/chunk-CRJV762I.mjs +0 -3102
  117. package/dist/client/chunk-LXRCF5OS.mjs +0 -25
  118. package/dist/client/chunk-UBDQUZZV.mjs +0 -1
@@ -0,0 +1,1317 @@
1
+ import thisCss from "./cosmos-navigator.css";
2
+ import thisHtml from "./cosmos-navigator.html";
3
+
4
+ import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
+ import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
6
+ import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
7
+ import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
8
+ import { getSpaceIdFromIb } from "@ibgib/core-gib/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace-helper.mjs";
9
+ import { createCosmosIbGib, addGalaxyToCosmos } from "@ibgib/core-gib/dist/cosmology/cosmos/cosmos-helpers.mjs";
10
+ import { getSpaceQualifiedIbGibAddr, getIbGibFromSpace } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
11
+ import { CosmosIbGib_V1 } from "@ibgib/core-gib/dist/cosmology/cosmos/cosmos-types.mjs";
12
+ import { GalaxyPresetType } from "@ibgib/core-gib/dist/cosmology/galaxy/galaxy-configs.mjs";
13
+ import { createGalaxyIbGib } from "@ibgib/core-gib/dist/cosmology/galaxy/galaxy-helpers.mjs";
14
+ import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase, } from "@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs";
15
+ import {
16
+ ElementsBase, IbGibDynamicComponentInstance,
17
+ IbGibDynamicComponentInstanceInitOpts,
18
+ } from "@ibgib/web-gib/dist/ui/component/component-types.mjs";
19
+ import { getComponentCtorArg } from "@ibgib/web-gib/dist/app-bootstrap/init-orchestration.mjs";
20
+ import { promptForText } from "@ibgib/web-gib/dist/helpers.web.mjs";
21
+
22
+ import { getPlaygroundMetaspaceAndSpace, seedInMemoryCosmosWorkspace } from "../../../cosmos/seed-cosmos-workspace.mjs";
23
+ import { executeVcsInit } from "@ibgib/core-gib/dist/cosmology/branching/commands/core-init-cmd.mjs";
24
+ import { GalaxyIbGib_V1 } from "@ibgib/core-gib/dist/cosmology/galaxy/galaxy-types.mjs";
25
+ import { getSpaceGibRouterSvc } from "../../router/index.mjs";
26
+
27
+ export const COSMOS_NAVIGATOR_COMPONENT_NAME = 'space-gib-cosmos-navigator';
28
+
29
+ export interface AttachedSpace {
30
+ id: string;
31
+ type: GalaxyPresetType | 'code' | 'text';
32
+ name: string;
33
+ shortLabel: string;
34
+ icon: string;
35
+ }
36
+
37
+ export interface CosmosInfo {
38
+ id: string;
39
+ name: string;
40
+ ibGibAddr?: IbGibAddr;
41
+ spaces: AttachedSpace[];
42
+ createdAt: number;
43
+ isTemporary?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Metadata factory for space-gib-cosmos-navigator Web Component.
48
+ */
49
+ export class CosmosNavigatorComponentMeta extends IbGibDynamicComponentMetaBase {
50
+ protected lc: string = `[${CosmosNavigatorComponentMeta.name}]`;
51
+
52
+ routeRegExp?: RegExp = new RegExp(`^${COSMOS_NAVIGATOR_COMPONENT_NAME}$`);
53
+ componentName = COSMOS_NAVIGATOR_COMPONENT_NAME;
54
+
55
+ constructor() {
56
+ super(getComponentCtorArg());
57
+ if (!customElements.get(this.componentName)) {
58
+ customElements.define(this.componentName, CosmosNavigatorComponentInstance);
59
+ }
60
+ }
61
+
62
+ async createInstance({
63
+ path,
64
+ ibGibAddr
65
+ }: {
66
+ path: string;
67
+ ibGibAddr: IbGibAddr;
68
+ }): Promise<IbGibDynamicComponentInstance<IbGib_V1>> {
69
+ const component = new CosmosNavigatorComponentInstance();
70
+ await component.initialize({
71
+ ibGibAddr,
72
+ meta: this,
73
+ html: thisHtml,
74
+ css: [thisCss],
75
+ });
76
+ return component;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * References to DOM elements within the component's Shadow Root.
82
+ */
83
+ export interface CosmosNavigatorElements extends ElementsBase {
84
+ rootEl: HTMLDivElement;
85
+ canvasEl: HTMLCanvasElement;
86
+ polycosmosStageEl: HTMLDivElement;
87
+ singularityContainerEl: HTMLDivElement;
88
+ btnSingularityEl: HTMLButtonElement;
89
+ singularityLabelEl: HTMLSpanElement;
90
+ orbitTrackEl: HTMLDivElement;
91
+ orbitingCosmosesContainerEl: HTMLDivElement;
92
+ activeCosmosStageEl: HTMLDivElement;
93
+ nodePitchWrapperEl: HTMLDivElement;
94
+ btnNodePitchEl: HTMLButtonElement;
95
+ nodeIdentityWrapperEl: HTMLDivElement;
96
+ btnNodeIdentityEl: HTMLButtonElement;
97
+ nodeSuperspaceWrapperEl: HTMLDivElement;
98
+ btnNodeSuperspaceEl: HTMLButtonElement;
99
+ activeCosmosTitleCaptionEl: HTMLSpanElement;
100
+ activeCosmosTempBadgeEl?: HTMLSpanElement;
101
+ superspaceSatellitesEl: HTMLDivElement;
102
+ btnSatCodeEl?: HTMLButtonElement;
103
+ btnSatTextEl?: HTMLButtonElement;
104
+ nodeAgentWrapperEl: HTMLDivElement;
105
+ btnNodeAgentEl: HTMLButtonElement;
106
+ }
107
+
108
+ /**
109
+ * Concrete implementation of the unified Cosmos & Polycosmos Navigator component.
110
+ */
111
+ export class CosmosNavigatorComponentInstance
112
+ extends IbGibDynamicComponentInstanceBase<IbGib_V1, CosmosNavigatorElements>
113
+ implements IbGibDynamicComponentInstance<IbGib_V1, CosmosNavigatorElements> {
114
+
115
+ protected lc: string = `[${CosmosNavigatorComponentInstance.name}]`;
116
+
117
+ private get el(): CosmosNavigatorElements {
118
+ if (!this.elements) {
119
+ throw new Error(`[${CosmosNavigatorComponentInstance.name}] elements not initialized.`);
120
+ }
121
+ return this.elements;
122
+ }
123
+
124
+ // State
125
+ private cosmoses: CosmosInfo[] = [];
126
+ private activeCosmosId: string | null = null;
127
+ private zoomState: 'polycosmos' | 'cosmos' = 'polycosmos';
128
+ private initialOnboardingDone: boolean = false;
129
+ private isSpaceLaunching: boolean = false;
130
+
131
+ // Canvas & Starfield State
132
+ public hasBigBangExploded: boolean = false;
133
+ private starfieldFadeAlpha: number = 0;
134
+ private ctx: CanvasRenderingContext2D | null = null;
135
+ private animFrameId: number | null = null;
136
+ private resizeObserver: ResizeObserver | null = null;
137
+ private stars: Array<{ x: number; y: number; r: number; alpha: number; speed: number; phase: number }> = [];
138
+ private particles: Array<{
139
+ x: number;
140
+ y: number;
141
+ vx: number;
142
+ vy: number;
143
+ r: number;
144
+ color: string;
145
+ alpha: number;
146
+ decay: number;
147
+ }> = [];
148
+ private shockwaveRadius = 0;
149
+ private shockwaveAlpha = 0;
150
+
151
+ constructor() {
152
+ super();
153
+ }
154
+
155
+ override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise<void> {
156
+ await super.initialize(opts);
157
+ }
158
+
159
+ override async disconnected(): Promise<void> {
160
+ const lc = `${this.lc}[${this.disconnected.name}]`;
161
+ try {
162
+ this.clearAutoDockTimer();
163
+ if (this.animFrameId) {
164
+ cancelAnimationFrame(this.animFrameId);
165
+ this.animFrameId = null;
166
+ }
167
+ if (this.resizeObserver) {
168
+ this.resizeObserver.disconnect();
169
+ this.resizeObserver = null;
170
+ }
171
+ } catch (error) {
172
+ console.error(`${lc} ${extractErrorMsg(error)}`);
173
+ }
174
+ }
175
+
176
+ protected override async connectedCallback_reconnectComponent(): Promise<void> {
177
+ await super.connectedCallback_reconnectComponent();
178
+ this.ensureStars();
179
+ this.renderCurrentState();
180
+ }
181
+
182
+ override async created(): Promise<void> {
183
+ const lc = `${this.lc}[${this.created.name}]`;
184
+ try {
185
+ const shadow = this.shadowRoot!;
186
+ this.elements = {
187
+ rootEl: shadow.getElementById('cosmos-navigator-root') as HTMLDivElement,
188
+ canvasEl: shadow.getElementById('starfield-canvas') as HTMLCanvasElement,
189
+ polycosmosStageEl: shadow.getElementById('polycosmos-stage') as HTMLDivElement,
190
+ singularityContainerEl: shadow.getElementById('singularity-container') as HTMLDivElement,
191
+ btnSingularityEl: shadow.getElementById('btn-singularity') as HTMLButtonElement,
192
+ singularityLabelEl: shadow.getElementById('singularity-label') as HTMLSpanElement,
193
+ orbitTrackEl: shadow.getElementById('orbit-track') as HTMLDivElement,
194
+ orbitingCosmosesContainerEl: shadow.getElementById('orbiting-cosmoses-container') as HTMLDivElement,
195
+ activeCosmosStageEl: shadow.getElementById('active-cosmos-stage') as HTMLDivElement,
196
+ nodePitchWrapperEl: shadow.getElementById('node-pitch-wrapper') as HTMLDivElement,
197
+ btnNodePitchEl: shadow.getElementById('btn-node-pitch') as HTMLButtonElement,
198
+ nodeIdentityWrapperEl: shadow.getElementById('node-identity-wrapper') as HTMLDivElement,
199
+ btnNodeIdentityEl: shadow.getElementById('btn-node-identity') as HTMLButtonElement,
200
+ nodeSuperspaceWrapperEl: shadow.getElementById('node-superspace-wrapper') as HTMLDivElement,
201
+ btnNodeSuperspaceEl: shadow.getElementById('btn-node-superspace') as HTMLButtonElement,
202
+ activeCosmosTitleCaptionEl: shadow.getElementById('active-cosmos-title-caption') as HTMLSpanElement,
203
+ activeCosmosTempBadgeEl: shadow.getElementById('active-cosmos-temp-badge') as HTMLSpanElement,
204
+ superspaceSatellitesEl: shadow.getElementById('superspace-satellites') as HTMLDivElement,
205
+ btnSatCodeEl: shadow.getElementById('btn-sat-code') as HTMLButtonElement,
206
+ btnSatTextEl: shadow.getElementById('btn-sat-text') as HTMLButtonElement,
207
+ nodeAgentWrapperEl: shadow.getElementById('node-agent-wrapper') as HTMLDivElement,
208
+ btnNodeAgentEl: shadow.getElementById('btn-node-agent') as HTMLButtonElement,
209
+ contentEl: shadow.getElementById('cosmos-navigator-root') as HTMLDivElement,
210
+ };
211
+
212
+ this.loadState();
213
+ this.initCanvas();
214
+ this.bindHandlers();
215
+ this.renderCurrentState();
216
+
217
+ } catch (error) {
218
+ console.error(`${lc} ${extractErrorMsg(error)}`);
219
+ }
220
+ }
221
+
222
+ private loadState(): void {
223
+ // In-memory only for Phase 1 testing; backing ibgibs will handle persistence in Phase 2
224
+ this.cosmoses = [];
225
+ this.activeCosmosId = null;
226
+ this.zoomState = 'polycosmos';
227
+ this.hasBigBangExploded = false;
228
+ this.starfieldFadeAlpha = 0;
229
+
230
+ // Clear any stale keys from earlier runs so reload presents a clean slate
231
+ try {
232
+ localStorage.removeItem('space-gib-cosmoses');
233
+ localStorage.removeItem('space-gib-active-cosmos-id');
234
+ localStorage.removeItem('space-gib-mode');
235
+ } catch {
236
+ // ignore
237
+ }
238
+ }
239
+
240
+ private saveState(): void {
241
+ // In-memory only for Phase 1 testing; backing ibgibs will handle persistence in Phase 2
242
+ }
243
+
244
+ private initCanvas(): void {
245
+ if (!this.el.canvasEl) return;
246
+ this.ctx = this.el.canvasEl.getContext('2d');
247
+ if (!this.ctx) return;
248
+
249
+ this.resizeCanvas();
250
+
251
+ if (typeof window !== 'undefined' && 'ResizeObserver' in window && this.el.rootEl) {
252
+ this.resizeObserver = new ResizeObserver(() => {
253
+ this.resizeCanvas();
254
+ });
255
+ this.resizeObserver.observe(this.el.rootEl);
256
+ }
257
+
258
+ window.addEventListener('resize', () => this.resizeCanvas());
259
+
260
+ this.initStars();
261
+ this.startCanvasLoop();
262
+ }
263
+
264
+ public ensureStars(): void {
265
+ if (!this.animFrameId) {
266
+ this.startCanvasLoop();
267
+ }
268
+ this.resizeCanvas();
269
+ if (this.stars.length === 0) {
270
+ this.initStars();
271
+ }
272
+ setTimeout(() => {
273
+ this.resizeCanvas();
274
+ if (this.stars.length === 0) {
275
+ this.initStars();
276
+ }
277
+ }, 50);
278
+ setTimeout(() => {
279
+ this.resizeCanvas();
280
+ }, 200);
281
+ }
282
+
283
+ private resizeCanvas(): void {
284
+ if (!this.el.canvasEl) return;
285
+ const parent = this.el.rootEl || this.el.canvasEl.parentElement;
286
+ const parentW = parent && parent.clientWidth > 0 ? parent.clientWidth : window.innerWidth;
287
+ const parentH = parent && parent.clientHeight > 0 ? parent.clientHeight : 500;
288
+ const targetW = Math.max(300, Math.floor(parentW));
289
+ const targetH = Math.max(300, Math.floor(parentH));
290
+
291
+ if (this.el.canvasEl.width !== targetW || this.el.canvasEl.height !== targetH) {
292
+ this.el.canvasEl.width = targetW;
293
+ this.el.canvasEl.height = targetH;
294
+ if (this.stars.length === 0) {
295
+ this.initStars();
296
+ }
297
+ }
298
+ }
299
+
300
+ private initStars(): void {
301
+ if (!this.el.canvasEl) return;
302
+ const w = Math.max(300, this.el.canvasEl.width);
303
+ const h = Math.max(300, this.el.canvasEl.height);
304
+ this.stars = [];
305
+ const count = Math.max(100, Math.min(260, Math.floor((w * h) / 3600)));
306
+ for (let i = 0; i < count; i++) {
307
+ this.stars.push({
308
+ x: Math.random() * w,
309
+ y: Math.random() * h,
310
+ r: Math.random() * 1.6 + 0.4,
311
+ alpha: Math.random() * 0.75 + 0.25,
312
+ speed: Math.random() * 0.008 + 0.003,
313
+ phase: Math.random() * Math.PI * 2,
314
+ });
315
+ }
316
+ }
317
+
318
+ private startCanvasLoop(): void {
319
+ if (this.animFrameId) return;
320
+
321
+ const loop = () => {
322
+ this.renderCanvasFrame();
323
+ this.animFrameId = requestAnimationFrame(loop);
324
+ };
325
+ this.animFrameId = requestAnimationFrame(loop);
326
+ }
327
+
328
+ private renderCanvasFrame(): void {
329
+ if (!this.ctx || !this.el.canvasEl) return;
330
+ const ctx = this.ctx;
331
+ const w = this.el.canvasEl.width;
332
+ const h = this.el.canvasEl.height;
333
+
334
+ ctx.clearRect(0, 0, w, h);
335
+
336
+ // 1. Draw starfield (only after Big Bang explosion)
337
+ if (this.hasBigBangExploded) {
338
+ if (this.starfieldFadeAlpha < 1.0) {
339
+ this.starfieldFadeAlpha = Math.min(1.0, this.starfieldFadeAlpha + 0.02);
340
+ }
341
+ for (let i = 0; i < this.stars.length; i++) {
342
+ const s = this.stars[i];
343
+ s.phase += s.speed;
344
+ const currentAlpha = Math.max(0.1, Math.min(1.0, s.alpha + Math.sin(s.phase) * 0.35)) * this.starfieldFadeAlpha;
345
+
346
+ ctx.fillStyle = `rgba(255, 255, 255, ${currentAlpha})`;
347
+ ctx.beginPath();
348
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
349
+ ctx.fill();
350
+
351
+ // Subtle drift
352
+ s.y -= 0.08;
353
+ if (s.y < 0) {
354
+ s.y = h;
355
+ s.x = Math.random() * w;
356
+ }
357
+ }
358
+ }
359
+
360
+ // 2. Shockwave ring (Big Bang explosion)
361
+ if (this.shockwaveAlpha > 0.01) {
362
+ ctx.strokeStyle = `rgba(167, 139, 250, ${this.shockwaveAlpha})`;
363
+ ctx.lineWidth = 4 * this.shockwaveAlpha;
364
+ ctx.beginPath();
365
+ ctx.arc(w / 2, h / 2, this.shockwaveRadius, 0, Math.PI * 2);
366
+ ctx.stroke();
367
+
368
+ // Secondary golden/amber inner shockwave ring
369
+ ctx.strokeStyle = `rgba(251, 191, 36, ${this.shockwaveAlpha * 0.75})`;
370
+ ctx.lineWidth = 2.5 * this.shockwaveAlpha;
371
+ ctx.beginPath();
372
+ ctx.arc(w / 2, h / 2, Math.max(0, this.shockwaveRadius - 16), 0, Math.PI * 2);
373
+ ctx.stroke();
374
+
375
+ this.shockwaveRadius += 10;
376
+ // Slower decay: lasts ~1 second longer than before
377
+ this.shockwaveAlpha *= 0.975;
378
+ }
379
+
380
+ // 3. Explosion Particles
381
+ for (let i = this.particles.length - 1; i >= 0; i--) {
382
+ const p = this.particles[i];
383
+ p.x += p.vx;
384
+ p.y += p.vy;
385
+ p.vx *= 0.978;
386
+ p.vy *= 0.978;
387
+ p.alpha -= p.decay;
388
+
389
+ if (p.alpha <= 0.01) {
390
+ this.particles.splice(i, 1);
391
+ continue;
392
+ }
393
+
394
+ ctx.fillStyle = p.color.replace('rgb', 'rgba').replace(')', `, ${p.alpha})`);
395
+ ctx.beginPath();
396
+ ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
397
+ ctx.fill();
398
+ }
399
+ }
400
+
401
+ public triggerBigBangExplosion(): void {
402
+ this.hasBigBangExploded = true;
403
+ if (!this.el.canvasEl) return;
404
+ const w = this.el.canvasEl.width;
405
+ const h = this.el.canvasEl.height;
406
+ const cx = w / 2;
407
+ const cy = h / 2;
408
+
409
+ this.shockwaveRadius = 10;
410
+ this.shockwaveAlpha = 1.0;
411
+
412
+ this.particles = [];
413
+ const colors = [
414
+ 'rgb(255, 255, 255)',
415
+ 'rgb(167, 139, 250)',
416
+ 'rgb(124, 92, 252)',
417
+ 'rgb(56, 189, 248)',
418
+ 'rgb(236, 72, 153)',
419
+ 'rgb(251, 191, 36)',
420
+ ];
421
+
422
+ const particleCount = 140;
423
+ for (let i = 0; i < particleCount; i++) {
424
+ const angle = Math.random() * Math.PI * 2;
425
+ const speed = Math.random() * 11 + 2.5;
426
+ this.particles.push({
427
+ x: cx,
428
+ y: cy,
429
+ vx: Math.cos(angle) * speed,
430
+ vy: Math.sin(angle) * speed,
431
+ r: Math.random() * 3.5 + 1.2,
432
+ color: colors[Math.floor(Math.random() * colors.length)],
433
+ alpha: 1.0,
434
+ // Slower decay: lasts ~1 second longer
435
+ decay: Math.random() * 0.007 + 0.005,
436
+ });
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Staged sequence for the Big Bang initiation:
442
+ * 1. Singularity compresses with high velocity spinning firecracker rotation (1000ms).
443
+ * 2. Explosion triggers: shockwave & particles burst outward across the canvas.
444
+ * 3. Dispatches 'space-gib-big-bang-exploded' so the Shell can fade in header & footer.
445
+ * 4. Cosmos & escaping buttons blossom out with the explosion shockwave (120ms).
446
+ */
447
+ public triggerBigBangSequence(onComplete?: () => void): void {
448
+ const btn = this.el.btnSingularityEl;
449
+ if (!btn) {
450
+ this.triggerBigBangExplosion();
451
+ onComplete?.();
452
+ return;
453
+ }
454
+
455
+ // Stage 1: Gravitational compression with high-velocity firecracker rotation (1000ms)
456
+ btn.classList.add('compressing');
457
+
458
+ setTimeout(() => {
459
+ // Stage 2: Explosion burst (BOOM!)
460
+ btn.classList.remove('compressing');
461
+ btn.classList.add('exploded');
462
+
463
+ this.triggerBigBangExplosion();
464
+
465
+ // Dispatch event to Shell: the Big Bang explosion has hit!
466
+ this.dispatchEvent(new CustomEvent('space-gib-big-bang-exploded', {
467
+ bubbles: true,
468
+ composed: true,
469
+ }));
470
+
471
+ // Stage 3: Cosmos and buttons emerge directly with the explosion shockwave
472
+ setTimeout(() => {
473
+ onComplete?.();
474
+ }, 120);
475
+
476
+ setTimeout(() => {
477
+ btn.classList.remove('exploded');
478
+ }, 600);
479
+ }, 1000);
480
+ }
481
+
482
+ private bindHandlers(): void {
483
+ // Singularity Button click (Empty state Big Bang OR Polycosmos New Cosmos)
484
+ this.el.btnSingularityEl.addEventListener('click', () => {
485
+ this.triggerBigBangSequence(async () => {
486
+ if (this.cosmoses.length === 0) {
487
+ try {
488
+ const seeded = await seedInMemoryCosmosWorkspace();
489
+ if (!this.cosmoses.some(c => c.id === seeded.id)) {
490
+ this.cosmoses.push(seeded);
491
+ }
492
+ this.selectCosmos(seeded.id);
493
+ } catch (seedErr) {
494
+ console.error(`${this.lc} Failed to seed in-memory cosmos: ${extractErrorMsg(seedErr)}`);
495
+ throw seedErr;
496
+ }
497
+ } else {
498
+ const nextIndex = this.cosmoses.length + 1;
499
+ const newCosmos = await this.createCosmos(`cosmos-${nextIndex}`);
500
+ this.selectCosmos(newCosmos.id);
501
+ }
502
+ });
503
+ });
504
+
505
+
506
+
507
+ // Pitch Node Click (💰)
508
+ this.el.btnNodePitchEl?.addEventListener('click', (e) => {
509
+ e.stopPropagation();
510
+ this.handlePitchClick();
511
+ });
512
+
513
+ // Identity Node Click
514
+ this.el.btnNodeIdentityEl.addEventListener('click', (e) => {
515
+ e.stopPropagation();
516
+ this.handleIdentityClick();
517
+ });
518
+
519
+ // Agent Chat Click
520
+ this.el.btnNodeAgentEl.addEventListener('click', (e) => {
521
+ e.stopPropagation();
522
+ this.handleAgentClick();
523
+ });
524
+
525
+ // Code Galaxy Revolving Satellite Button Click (</>)
526
+ this.el.btnSatCodeEl?.addEventListener('click', async (e) => {
527
+ e.stopPropagation();
528
+ await this.handleSatCodeClick();
529
+ });
530
+
531
+ // Notes Galaxy Revolving Satellite Button Click (📝)
532
+ this.el.btnSatTextEl?.addEventListener('click', async (e) => {
533
+ e.stopPropagation();
534
+ await this.handleSatTextClick();
535
+ });
536
+
537
+ // Hover pause on satellites
538
+ [this.el.btnSatCodeEl, this.el.btnSatTextEl].forEach(btn => {
539
+ btn?.addEventListener('mouseenter', () => {
540
+ this.el.activeCosmosStageEl?.classList.add('satellites-hovered');
541
+ });
542
+ btn?.addEventListener('mouseleave', () => {
543
+ this.el.activeCosmosStageEl?.classList.remove('satellites-hovered');
544
+ });
545
+ });
546
+ }
547
+
548
+ public async handleSatCodeClick(): Promise<void> {
549
+ const activeCosmos = this.getActiveCosmos();
550
+ if (!activeCosmos) return;
551
+
552
+ const count = activeCosmos.spaces.filter(s => s.type === 'code').length;
553
+ const defaultName = count === 0 ? 'code-1' : `code-${count + 1}`;
554
+ const galaxyName = await promptForText({
555
+ title: 'Create Code Galaxy',
556
+ msg: `Name of the new Code Galaxy for [${activeCosmos.name}]:`,
557
+ defaultValue: defaultName,
558
+ cancelable: true,
559
+ });
560
+
561
+ if (!galaxyName) return;
562
+
563
+ const sourceBtn = this.el.btnSatCodeEl || this.el.btnNodeSuperspaceEl;
564
+ const sourceWrapper = (sourceBtn?.closest('.satellite-wrapper') as HTMLElement) || sourceBtn;
565
+
566
+ let space = activeCosmos.spaces.find(s => s.id === galaxyName || s.name === galaxyName);
567
+ if (!space) {
568
+ space = await this.createAndAttachGalaxy({
569
+ cosmosId: activeCosmos.id,
570
+ galaxyName,
571
+ discriminator: GalaxyPresetType.code,
572
+ skipEvent: true,
573
+ });
574
+ }
575
+
576
+ if (space) {
577
+ this.handleSpaceClick(space, sourceBtn, sourceWrapper);
578
+ }
579
+ }
580
+
581
+ public async handleSatTextClick(): Promise<void> {
582
+ const activeCosmos = this.getActiveCosmos();
583
+ if (!activeCosmos) return;
584
+
585
+ const count = activeCosmos.spaces.filter(s => s.type === 'text' || s.type === GalaxyPresetType.notes).length;
586
+ const defaultName = count === 0 ? 'notes-1' : `notes-${count + 1}`;
587
+ const galaxyName = await promptForText({
588
+ title: 'Create Notes Galaxy',
589
+ msg: `Name of the new Notes Galaxy for [${activeCosmos.name}]:`,
590
+ defaultValue: defaultName,
591
+ cancelable: true,
592
+ });
593
+
594
+ if (!galaxyName) return;
595
+
596
+ const sourceBtn = this.el.btnSatTextEl || this.el.btnNodeSuperspaceEl;
597
+ const sourceWrapper = (sourceBtn?.closest('.satellite-wrapper') as HTMLElement) || sourceBtn;
598
+
599
+ let space = activeCosmos.spaces.find(s => s.id === galaxyName || s.name === galaxyName);
600
+ if (!space) {
601
+ space = await this.createAndAttachGalaxy({
602
+ cosmosId: activeCosmos.id,
603
+ galaxyName,
604
+ discriminator: GalaxyPresetType.notes,
605
+ skipEvent: true,
606
+ });
607
+ }
608
+
609
+ if (space) {
610
+ this.handleSpaceClick(space, sourceBtn, sourceWrapper);
611
+ }
612
+ }
613
+
614
+ private launchProjectile({
615
+ sourceWrapperEl,
616
+ sourceBtnEl,
617
+ targetEl,
618
+ fallbackTarget,
619
+ emoji,
620
+ isPitch = false,
621
+ projectileType,
622
+ duration = 1600,
623
+ onApproach,
624
+ onDocked,
625
+ }: {
626
+ sourceWrapperEl: HTMLElement;
627
+ sourceBtnEl: HTMLElement;
628
+ targetEl?: HTMLElement | null;
629
+ fallbackTarget?: { x: number; y: number; width?: number; height?: number };
630
+ emoji: string;
631
+ isPitch?: boolean;
632
+ projectileType?: 'pitch' | 'identity' | 'agent' | 'code' | 'text';
633
+ duration?: number;
634
+ onApproach?: () => void;
635
+ onDocked: () => void;
636
+ }): void {
637
+ const fromRect = sourceBtnEl.getBoundingClientRect();
638
+ let toRect: { left: number; top: number; width: number; height: number };
639
+
640
+ if (targetEl) {
641
+ const r = targetEl.getBoundingClientRect();
642
+ if (r.width > 0 && r.height > 0) {
643
+ toRect = { left: r.left, top: r.top, width: r.width, height: r.height };
644
+ } else if (fallbackTarget) {
645
+ toRect = { left: fallbackTarget.x, top: fallbackTarget.y, width: fallbackTarget.width || 44, height: fallbackTarget.height || 44 };
646
+ } else {
647
+ toRect = { left: r.left, top: r.top, width: 44, height: 44 };
648
+ }
649
+ } else if (fallbackTarget) {
650
+ toRect = { left: fallbackTarget.x, top: fallbackTarget.y, width: fallbackTarget.width || 44, height: fallbackTarget.height || 44 };
651
+ } else {
652
+ onDocked();
653
+ return;
654
+ }
655
+
656
+ // Hide source wrapper in the canvas stage so only the flying projectile is seen
657
+ sourceWrapperEl.classList.add('node-is-escaping');
658
+
659
+ // Create the flying projectile on document.body (above all panels, headers, overflow:hidden)
660
+ const projectile = document.createElement('div');
661
+ const isPitchProj = isPitch || projectileType === 'pitch';
662
+ projectile.className = `cosmos-flying-projectile ${isPitchProj ? 'pitch-projectile' : ''}`;
663
+ projectile.style.position = 'fixed';
664
+ projectile.style.left = `${fromRect.left}px`;
665
+ projectile.style.top = `${fromRect.top}px`;
666
+ projectile.style.width = `${fromRect.width}px`;
667
+ projectile.style.height = `${fromRect.height}px`;
668
+ projectile.style.zIndex = '999999';
669
+ projectile.style.pointerEvents = 'none';
670
+ projectile.style.margin = '0';
671
+ projectile.style.borderRadius = '50%';
672
+ projectile.style.display = 'flex';
673
+ projectile.style.alignItems = 'center';
674
+ projectile.style.justifyContent = 'center';
675
+ projectile.style.willChange = 'transform, opacity';
676
+
677
+ if (projectileType === 'code') {
678
+ projectile.style.background = 'radial-gradient(circle, rgba(14, 165, 233, 0.45) 0%, rgba(15, 23, 42, 0.98) 100%)';
679
+ projectile.style.border = '2px solid rgba(56, 189, 248, 0.95)';
680
+ projectile.style.boxShadow = '0 0 28px rgba(56, 189, 248, 0.85), inset 0 0 14px rgba(56, 189, 248, 0.45)';
681
+ projectile.style.color = '#38bdf8';
682
+ projectile.style.fontSize = '1.25rem';
683
+ projectile.style.fontWeight = '700';
684
+ projectile.style.fontFamily = 'monospace';
685
+ } else if (projectileType === 'text') {
686
+ projectile.style.background = 'radial-gradient(circle, rgba(167, 139, 250, 0.45) 0%, rgba(15, 23, 42, 0.98) 100%)';
687
+ projectile.style.border = '2px solid rgba(167, 139, 250, 0.95)';
688
+ projectile.style.boxShadow = '0 0 28px rgba(167, 139, 250, 0.85), inset 0 0 14px rgba(167, 139, 250, 0.45)';
689
+ projectile.style.fontSize = '1.45rem';
690
+ } else if (isPitchProj) {
691
+ projectile.style.background = 'radial-gradient(circle, rgba(251, 191, 36, 0.3) 0%, rgba(217, 119, 6, 0.95) 100%)';
692
+ projectile.style.border = '2px solid rgba(251, 191, 36, 0.9)';
693
+ projectile.style.boxShadow = '0 0 28px rgba(251, 191, 36, 0.75), inset 0 0 14px rgba(251, 191, 36, 0.4)';
694
+ projectile.style.fontSize = '1.35rem';
695
+ } else {
696
+ projectile.style.background = 'radial-gradient(circle, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%)';
697
+ projectile.style.border = '2px solid rgba(124, 92, 252, 0.85)';
698
+ projectile.style.boxShadow = '0 0 28px rgba(124, 92, 252, 0.7), inset 0 0 15px rgba(124, 92, 252, 0.3)';
699
+ projectile.style.fontSize = '1.5rem';
700
+ }
701
+
702
+ const emojiSpan = document.createElement('span');
703
+ emojiSpan.textContent = emoji;
704
+ projectile.appendChild(emojiSpan);
705
+
706
+ document.body.appendChild(projectile);
707
+
708
+ if (onApproach && duration > 350) {
709
+ window.setTimeout(() => {
710
+ onApproach?.();
711
+ }, duration - 350);
712
+ }
713
+
714
+ const deltaX = (toRect.left + toRect.width / 2) - (fromRect.left + fromRect.width / 2);
715
+ const deltaY = (toRect.top + toRect.height / 2) - (fromRect.top + fromRect.height / 2);
716
+ const targetScale = Math.min(toRect.width / fromRect.width, toRect.height / fromRect.height) || 0.85;
717
+
718
+ // Smooth trajectory: fully visible throughout the flight
719
+ const anim = projectile.animate([
720
+ {
721
+ transform: 'translate(0px, 0px) scale(1)',
722
+ opacity: 1,
723
+ },
724
+ {
725
+ transform: `translate(${deltaX * 0.45}px, ${deltaY * 0.35}px) scale(${1 + (targetScale - 1) * 0.2})`,
726
+ opacity: 1,
727
+ offset: 0.45,
728
+ },
729
+ {
730
+ transform: `translate(${deltaX}px, ${deltaY}px) scale(${targetScale})`,
731
+ opacity: 1,
732
+ offset: 0.9,
733
+ },
734
+ {
735
+ transform: `translate(${deltaX}px, ${deltaY}px) scale(${targetScale})`,
736
+ opacity: 0,
737
+ offset: 1.0,
738
+ }
739
+ ], {
740
+ duration,
741
+ easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
742
+ fill: 'forwards',
743
+ });
744
+
745
+ anim.onfinish = () => {
746
+ projectile.remove();
747
+ if (projectileType === 'code' || projectileType === 'text') {
748
+ sourceWrapperEl.classList.remove('node-is-escaping');
749
+ }
750
+ onDocked();
751
+ };
752
+ }
753
+
754
+ public handlePitchClick(isAuto: boolean = false): void {
755
+ this.initialOnboardingDone = true;
756
+ if (!this.el.nodePitchWrapperEl || !this.el.btnNodePitchEl) return;
757
+ if (this.el.nodePitchWrapperEl.classList.contains('node-is-escaping')) return;
758
+
759
+ const targetEl = document.getElementById('btn-header-pitch');
760
+ const fallbackTarget = {
761
+ x: window.innerWidth / 2 - 26,
762
+ y: 8,
763
+ width: 52,
764
+ height: 52,
765
+ };
766
+
767
+ this.launchProjectile({
768
+ sourceWrapperEl: this.el.nodePitchWrapperEl,
769
+ sourceBtnEl: this.el.btnNodePitchEl,
770
+ targetEl,
771
+ fallbackTarget,
772
+ emoji: '💰',
773
+ isPitch: true,
774
+ duration: 1600,
775
+ onDocked: () => {
776
+ this.dispatchEvent(new CustomEvent('space-gib-pitch-docked', {
777
+ bubbles: true,
778
+ composed: true,
779
+ detail: { isAuto },
780
+ }));
781
+ }
782
+ });
783
+ }
784
+
785
+ public handleIdentityClick(isAuto: boolean = false): void {
786
+ this.initialOnboardingDone = true;
787
+ if (!this.el.nodeIdentityWrapperEl || !this.el.btnNodeIdentityEl) return;
788
+ if (this.el.nodeIdentityWrapperEl.classList.contains('node-is-escaping')) return;
789
+
790
+ const targetEl = document.getElementById('identity-header-container');
791
+ const fallbackTarget = {
792
+ x: window.innerWidth - 180,
793
+ y: 8,
794
+ width: 140,
795
+ height: 40,
796
+ };
797
+
798
+ this.launchProjectile({
799
+ sourceWrapperEl: this.el.nodeIdentityWrapperEl,
800
+ sourceBtnEl: this.el.btnNodeIdentityEl,
801
+ targetEl,
802
+ fallbackTarget,
803
+ emoji: '👤',
804
+ isPitch: false,
805
+ duration: 1600,
806
+ onDocked: () => {
807
+ this.dispatchEvent(new CustomEvent('space-gib-identity-docked', {
808
+ bubbles: true,
809
+ composed: true,
810
+ detail: { isAuto },
811
+ }));
812
+ }
813
+ });
814
+ }
815
+
816
+ public handleAgentClick(isAuto: boolean = false): void {
817
+ this.initialOnboardingDone = true;
818
+ if (!this.el.nodeAgentWrapperEl || !this.el.btnNodeAgentEl) return;
819
+ if (this.el.nodeAgentWrapperEl.classList.contains('node-is-escaping')) return;
820
+
821
+ const targetEl = document.getElementById('btn-footer-agent');
822
+ const fallbackTarget = {
823
+ x: window.innerWidth - 60,
824
+ y: window.innerHeight - 44,
825
+ width: 44,
826
+ height: 44,
827
+ };
828
+
829
+ this.launchProjectile({
830
+ sourceWrapperEl: this.el.nodeAgentWrapperEl,
831
+ sourceBtnEl: this.el.btnNodeAgentEl,
832
+ targetEl,
833
+ fallbackTarget,
834
+ emoji: '🤖',
835
+ isPitch: false,
836
+ projectileType: 'agent',
837
+ duration: 1600,
838
+ onDocked: () => {
839
+ this.dispatchEvent(new CustomEvent('space-gib-agent-docked', {
840
+ bubbles: true,
841
+ composed: true,
842
+ detail: { isAuto },
843
+ }));
844
+ }
845
+ });
846
+ }
847
+
848
+ public resetDriftAnimations(): void {
849
+ if (this.el.nodePitchWrapperEl) {
850
+ this.el.nodePitchWrapperEl.classList.remove('node-is-escaping');
851
+ this.el.nodePitchWrapperEl.style.opacity = '';
852
+ this.el.nodePitchWrapperEl.style.pointerEvents = '';
853
+ this.el.nodePitchWrapperEl.style.animation = 'none';
854
+ void this.el.nodePitchWrapperEl.offsetHeight; // trigger reflow
855
+ this.el.nodePitchWrapperEl.style.animation = '';
856
+ }
857
+ if (this.el.nodeIdentityWrapperEl) {
858
+ this.el.nodeIdentityWrapperEl.classList.remove('node-is-escaping');
859
+ this.el.nodeIdentityWrapperEl.style.opacity = '';
860
+ this.el.nodeIdentityWrapperEl.style.pointerEvents = '';
861
+ this.el.nodeIdentityWrapperEl.style.animation = 'none';
862
+ void this.el.nodeIdentityWrapperEl.offsetHeight; // trigger reflow
863
+ this.el.nodeIdentityWrapperEl.style.animation = '';
864
+ }
865
+ if (this.el.nodeAgentWrapperEl) {
866
+ this.el.nodeAgentWrapperEl.classList.remove('node-is-escaping');
867
+ this.el.nodeAgentWrapperEl.style.opacity = '';
868
+ this.el.nodeAgentWrapperEl.style.pointerEvents = '';
869
+ this.el.nodeAgentWrapperEl.style.animation = 'none';
870
+ void this.el.nodeAgentWrapperEl.offsetHeight; // trigger reflow
871
+ this.el.nodeAgentWrapperEl.style.animation = '';
872
+ }
873
+ if (this.el.nodeSuperspaceWrapperEl) {
874
+ this.el.nodeSuperspaceWrapperEl.style.animation = 'none';
875
+ void this.el.nodeSuperspaceWrapperEl.offsetHeight; // trigger reflow
876
+ this.el.nodeSuperspaceWrapperEl.style.animation = '';
877
+ }
878
+ if (this.el.superspaceSatellitesEl) {
879
+ const wrappers = this.el.superspaceSatellitesEl.querySelectorAll<HTMLElement>('.satellite-wrapper');
880
+ this.el.superspaceSatellitesEl.style.animation = 'none';
881
+ wrappers.forEach(w => w.style.animation = 'none');
882
+ void this.el.superspaceSatellitesEl.offsetHeight; // trigger reflow
883
+ this.el.superspaceSatellitesEl.style.animation = '';
884
+ wrappers.forEach(w => w.style.animation = '');
885
+ }
886
+ }
887
+
888
+ private autoDockTimer?: number;
889
+
890
+ public startAutoDockTimer(): void {
891
+ this.clearAutoDockTimer();
892
+ this.autoDockTimer = window.setTimeout(() => {
893
+ console.log(`${this.lc} 8-second auto-dock timer triggered.`);
894
+ this.initialOnboardingDone = true;
895
+ this.handlePitchClick(true);
896
+ setTimeout(() => this.handleIdentityClick(true), 250);
897
+ setTimeout(() => this.handleAgentClick(true), 500);
898
+ }, 8000);
899
+ }
900
+
901
+ public clearAutoDockTimer(): void {
902
+ if (this.autoDockTimer) {
903
+ clearTimeout(this.autoDockTimer);
904
+ this.autoDockTimer = undefined;
905
+ }
906
+ }
907
+
908
+ public handleSpaceClick(
909
+ typeOrSpace: AttachedSpace | 'code' | 'text',
910
+ sourceBtn?: HTMLElement,
911
+ sourceWrapper?: HTMLElement
912
+ ): AttachedSpace | null {
913
+ if (!this.activeCosmosId) return null;
914
+ const activeCosmos = this.getActiveCosmos();
915
+ if (!activeCosmos) { return null; }
916
+ if (this.isSpaceLaunching) return null;
917
+
918
+ this.clearAutoDockTimer();
919
+ this.initialOnboardingDone = true;
920
+ this.isSpaceLaunching = true;
921
+
922
+ const type = typeof typeOrSpace === 'string' ? typeOrSpace : typeOrSpace.type;
923
+ const space = typeof typeOrSpace === 'object' ? typeOrSpace : undefined;
924
+
925
+ const count = activeCosmos.spaces.filter(s => s.type === type).length;
926
+ const existingSpacesCount = activeCosmos.spaces.length;
927
+ const newSpace: AttachedSpace = space || {
928
+ id: `${type}-space-${count + 1}-${Date.now()}`,
929
+ type,
930
+ name: type === 'code' ? `Code Space ${count + 1}` : `Text Space ${count + 1}`,
931
+ shortLabel: type === 'code' ? `Code ${count + 1}` : `Text ${count + 1}`,
932
+ icon: type === 'code' ? '</>' : '📝',
933
+ };
934
+
935
+ if (!activeCosmos.spaces.some(s => s.id === newSpace.id)) {
936
+ activeCosmos.spaces.push(newSpace);
937
+ this.saveState();
938
+ }
939
+
940
+ const sourceBtnEl = sourceBtn || (this.el.superspaceSatellitesEl?.querySelector('.satellite-node-btn') as HTMLElement) || this.el.btnNodeSuperspaceEl;
941
+ const sourceWrapperEl = sourceWrapper || (sourceBtnEl?.closest('.satellite-wrapper') as HTMLElement) || sourceBtnEl;
942
+
943
+ const headerH = document.getElementById('main-header')?.getBoundingClientRect().height || 60;
944
+ const targetTop = headerH + 128 + (existingSpacesCount * 50);
945
+ const fallbackTarget = {
946
+ x: 5,
947
+ y: targetTop,
948
+ width: 42,
949
+ height: 42,
950
+ };
951
+
952
+ if (!sourceBtnEl || !sourceWrapperEl) {
953
+ this.isSpaceLaunching = false;
954
+ this.dispatchEvent(new CustomEvent('space-gib-space-added', {
955
+ detail: {
956
+ cosmosId: activeCosmos.id,
957
+ space: newSpace,
958
+ cosmos: activeCosmos,
959
+ },
960
+ bubbles: true,
961
+ composed: true,
962
+ }));
963
+ return newSpace;
964
+ }
965
+
966
+ // Make activity bar visible BEFORE the zoom flight starts
967
+ window.dispatchEvent(new CustomEvent('space-gib-activity-bar-expand-requested', {
968
+ bubbles: true,
969
+ composed: true,
970
+ }));
971
+
972
+ setTimeout(() => {
973
+ this.launchProjectile({
974
+ sourceWrapperEl,
975
+ sourceBtnEl,
976
+ fallbackTarget,
977
+ emoji: newSpace.icon || (type === 'code' ? '</>' : '📝'),
978
+ projectileType: type === 'code' ? 'code' : 'text',
979
+ duration: 1400,
980
+ onDocked: () => {
981
+ this.isSpaceLaunching = false;
982
+ sourceWrapperEl.classList.remove('node-is-escaping');
983
+ this.dispatchEvent(new CustomEvent('space-gib-space-added', {
984
+ detail: {
985
+ cosmosId: activeCosmos.id,
986
+ space: newSpace,
987
+ cosmos: activeCosmos,
988
+ },
989
+ bubbles: true,
990
+ composed: true,
991
+ }));
992
+ }
993
+ });
994
+ }, 180);
995
+
996
+ return newSpace;
997
+ }
998
+
999
+ public async createAndAttachGalaxy({
1000
+ cosmosId,
1001
+ galaxyName,
1002
+ discriminator,
1003
+ skipEvent = false,
1004
+ }: {
1005
+ cosmosId?: string;
1006
+ galaxyName: string;
1007
+ discriminator?: GalaxyPresetType | 'code' | 'notes';
1008
+ skipEvent?: boolean;
1009
+ }): Promise<AttachedSpace | undefined> {
1010
+ const cosmos = cosmosId ? this.cosmoses.find(c => c.id === cosmosId) : this.getActiveCosmos();
1011
+ if (!cosmos) return undefined;
1012
+
1013
+ try {
1014
+ const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
1015
+ const spaceId = space?.ib ? getSpaceIdFromIb(space.ib) || 'temp_space' : 'temp_space';
1016
+
1017
+ // 1. Fetch Cosmos IbGib
1018
+ let cosmosIbGib: CosmosIbGib_V1 | undefined;
1019
+ if (cosmos.ibGibAddr) {
1020
+ cosmosIbGib = await getIbGibFromSpace<CosmosIbGib_V1>({ addr: cosmos.ibGibAddr, space });
1021
+ }
1022
+ if (!cosmosIbGib) {
1023
+ // Try by name in router cache
1024
+ const router = getSpaceGibRouterSvc();
1025
+ cosmosIbGib = router.cosmosManifestCache?.get(cosmos.name);
1026
+ }
1027
+ if (!cosmosIbGib) {
1028
+ throw new Error(`cosmosIbGib not found for cosmos "${cosmos.name}" (addr: ${cosmos.ibGibAddr || 'none'}). (E: 661ab8c2b7b8800be247f588b17ee726)`);
1029
+ }
1030
+
1031
+ // 2. Create Galaxy IbGib
1032
+ let galaxyIbGib: GalaxyIbGib_V1;
1033
+ const isCode = discriminator === GalaxyPresetType.code;
1034
+ if (isCode) {
1035
+ const initRes = await executeVcsInit({
1036
+ metaspace,
1037
+ localSpace: space,
1038
+ galaxyName,
1039
+ branchName: 'main',
1040
+ });
1041
+ galaxyIbGib = initRes.galaxyIbGib;
1042
+ } else {
1043
+ galaxyIbGib = await createGalaxyIbGib({
1044
+ galaxyName,
1045
+ discriminator: GalaxyPresetType.notes,
1046
+ space,
1047
+ metaspace,
1048
+ });
1049
+ }
1050
+
1051
+ // 3. Add galaxy to cosmos manifest
1052
+ const sqGalaxyAddr = getSpaceQualifiedIbGibAddr({
1053
+ spaceId,
1054
+ ibGibAddr: getIbGibAddr({ ibGib: galaxyIbGib }),
1055
+ });
1056
+ cosmosIbGib = await addGalaxyToCosmos({
1057
+ cosmosIbGib,
1058
+ sqGalaxyAddr,
1059
+ galaxyIbGib,
1060
+ space,
1061
+ metaspace,
1062
+ });
1063
+
1064
+ // 4. Update cosmos ibGibAddr and router manifest cache
1065
+ cosmos.ibGibAddr = getIbGibAddr({ ibGib: cosmosIbGib });
1066
+ getSpaceGibRouterSvc().setCosmosManifest(cosmos.name, cosmosIbGib);
1067
+
1068
+ // 5. Update local cosmos state
1069
+ const newSpace: AttachedSpace = {
1070
+ id: galaxyName,
1071
+ type: isCode ? GalaxyPresetType.code : GalaxyPresetType.notes,
1072
+ name: galaxyName,
1073
+ shortLabel: galaxyName,
1074
+ icon: isCode ? '</>' : '📝',
1075
+ };
1076
+ if (!cosmos.spaces.some(s => s.id === galaxyName)) {
1077
+ cosmos.spaces.push(newSpace);
1078
+ }
1079
+ this.saveState();
1080
+
1081
+ // 7. Dispatch space added event if not skipped (e.g. handleSpaceClick dispatches on projectile dock)
1082
+ if (!skipEvent) {
1083
+ this.dispatchEvent(new CustomEvent('space-gib-space-added', {
1084
+ detail: {
1085
+ cosmosId: cosmos.id,
1086
+ space: newSpace,
1087
+ cosmos,
1088
+ },
1089
+ bubbles: true,
1090
+ composed: true,
1091
+ }));
1092
+ }
1093
+
1094
+ return newSpace;
1095
+ } catch (err) {
1096
+ console.error(`${this.lc} Failed to create and attach galaxy: ${extractErrorMsg(err)}`);
1097
+ throw err;
1098
+ }
1099
+ }
1100
+
1101
+ public async createCosmos(name?: string, isTemporary?: boolean): Promise<CosmosInfo> {
1102
+ const cosmosName = name || `cosmos-${this.cosmoses.length + 1}`;
1103
+ const hasIdentity = Boolean((globalThis as any).ibgib?.identity?.domainIdentity);
1104
+ const temp = isTemporary !== undefined ? isTemporary : !hasIdentity;
1105
+
1106
+ const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
1107
+ const cosmosIbGib = await createCosmosIbGib({
1108
+ cosmosName,
1109
+ description: `${cosmosName} Cosmos`,
1110
+ space,
1111
+ metaspace,
1112
+ });
1113
+ const ibGibAddr = getIbGibAddr({ ibGib: cosmosIbGib });
1114
+ getSpaceGibRouterSvc().setCosmosManifest(cosmosName, cosmosIbGib);
1115
+
1116
+ const newCosmos: CosmosInfo = {
1117
+ id: cosmosName,
1118
+ name: cosmosName,
1119
+ ibGibAddr,
1120
+ isTemporary: temp,
1121
+ spaces: [],
1122
+ createdAt: Date.now(),
1123
+ };
1124
+
1125
+ this.cosmoses.push(newCosmos);
1126
+ this.activeCosmosId = newCosmos.id;
1127
+ this.saveState();
1128
+ return newCosmos;
1129
+ }
1130
+
1131
+ public selectCosmos(cosmosId: string): void {
1132
+ const cosmos = this.cosmoses.find(c => c.id === cosmosId || c.name === cosmosId);
1133
+ if (!cosmos) { return; }
1134
+
1135
+ this.activeCosmosId = cosmos.id;
1136
+ this.saveState();
1137
+ this.setZoomState('cosmos', cosmos.id);
1138
+ }
1139
+
1140
+ public async findCosmos(cosmosNameOrId: string): Promise<CosmosInfo | undefined> {
1141
+ let existing = this.cosmoses.find(c => c.id === cosmosNameOrId || c.name === cosmosNameOrId);
1142
+ if (existing) { return existing; }
1143
+
1144
+ if (cosmosNameOrId === 'welcome-cosmos') {
1145
+ const seeded = await seedInMemoryCosmosWorkspace();
1146
+ if (!this.cosmoses.some(c => c.id === seeded.id)) {
1147
+ this.cosmoses.push(seeded);
1148
+ }
1149
+ return seeded;
1150
+ }
1151
+
1152
+ const router = getSpaceGibRouterSvc();
1153
+ const cachedManifest = router.getCachedCosmosManifest(cosmosNameOrId);
1154
+ if (cachedManifest) {
1155
+ const ibGibAddr = getIbGibAddr({ ibGib: cachedManifest });
1156
+ const restoredCosmos: CosmosInfo = {
1157
+ id: cosmosNameOrId,
1158
+ name: cosmosNameOrId,
1159
+ ibGibAddr,
1160
+ spaces: [],
1161
+ createdAt: Date.now(),
1162
+ };
1163
+ this.cosmoses.push(restoredCosmos);
1164
+ return restoredCosmos;
1165
+ }
1166
+
1167
+ return undefined;
1168
+ }
1169
+
1170
+ public async ensureCosmos(cosmosId: string): Promise<CosmosInfo | undefined> {
1171
+ return this.findCosmos(cosmosId);
1172
+ }
1173
+
1174
+ public setZoomState(state: 'polycosmos' | 'cosmos', cosmosId?: string): void {
1175
+ if (cosmosId) {
1176
+ this.activeCosmosId = cosmosId;
1177
+ this.saveState();
1178
+ }
1179
+
1180
+ this.zoomState = state;
1181
+ this.renderCurrentState();
1182
+
1183
+ if (state === 'polycosmos') {
1184
+ this.dispatchEvent(new CustomEvent('space-gib-polycosmos-activated', {
1185
+ detail: { cosmoses: this.cosmoses, activeCosmosId: this.activeCosmosId },
1186
+ bubbles: true,
1187
+ composed: true,
1188
+ }));
1189
+ } else {
1190
+ const activeCosmos = this.getActiveCosmos();
1191
+ if (activeCosmos) {
1192
+ this.dispatchEvent(new CustomEvent('space-gib-cosmos-selected', {
1193
+ detail: { cosmos: activeCosmos },
1194
+ bubbles: true,
1195
+ composed: true,
1196
+ }));
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ private renderCurrentState(): void {
1202
+ if (this.zoomState === 'cosmos' || this.cosmoses.length > 0) {
1203
+ this.hasBigBangExploded = true;
1204
+ this.starfieldFadeAlpha = 1.0;
1205
+ }
1206
+
1207
+ if (this.zoomState === 'polycosmos') {
1208
+ this.clearAutoDockTimer();
1209
+ this.el.rootEl.classList.remove('zoom-cosmos');
1210
+ this.el.rootEl.classList.add('zoom-polycosmos');
1211
+
1212
+ this.el.polycosmosStageEl.classList.remove('hidden');
1213
+ this.el.activeCosmosStageEl.classList.add('hidden');
1214
+
1215
+ if (this.cosmoses.length === 0) {
1216
+ // Primordial 0-state
1217
+ this.el.orbitTrackEl.classList.add('hidden');
1218
+ this.el.singularityLabelEl.textContent = '';
1219
+ } else {
1220
+ // Multi-cosmos Polycosmos
1221
+ this.el.orbitTrackEl.classList.remove('hidden');
1222
+ this.el.singularityLabelEl.textContent = '+ New Cosmos';
1223
+ this.renderOrbitNodes();
1224
+ }
1225
+ } else {
1226
+ // Zoomed-in Cosmos
1227
+ this.el.rootEl.classList.remove('zoom-polycosmos');
1228
+ this.el.rootEl.classList.add('zoom-cosmos');
1229
+
1230
+ this.el.polycosmosStageEl.classList.add('hidden');
1231
+ this.el.activeCosmosStageEl.classList.remove('hidden');
1232
+
1233
+ const showInitialNodes = !this.initialOnboardingDone && this.cosmoses.length <= 1;
1234
+
1235
+ if (this.el.nodePitchWrapperEl) {
1236
+ this.el.nodePitchWrapperEl.style.display = showInitialNodes ? '' : 'none';
1237
+ }
1238
+ if (this.el.nodeIdentityWrapperEl) {
1239
+ this.el.nodeIdentityWrapperEl.style.display = showInitialNodes ? '' : 'none';
1240
+ }
1241
+ if (this.el.nodeAgentWrapperEl) {
1242
+ this.el.nodeAgentWrapperEl.style.display = showInitialNodes ? '' : 'none';
1243
+ }
1244
+
1245
+ if (showInitialNodes) {
1246
+ this.resetDriftAnimations();
1247
+ this.startAutoDockTimer();
1248
+ } else {
1249
+ this.clearAutoDockTimer();
1250
+ }
1251
+
1252
+ const activeCosmos = this.getActiveCosmos();
1253
+ if (activeCosmos) {
1254
+ this.el.activeCosmosTitleCaptionEl.textContent = activeCosmos.name;
1255
+ if (activeCosmos.isTemporary) {
1256
+ this.el.activeCosmosTempBadgeEl?.classList.remove('hidden');
1257
+ } else {
1258
+ this.el.activeCosmosTempBadgeEl?.classList.add('hidden');
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ private renderOrbitNodes(): void {
1265
+ const container = this.el.orbitingCosmosesContainerEl;
1266
+ if (!container) { return; }
1267
+
1268
+ container.innerHTML = '';
1269
+ const N = this.cosmoses.length;
1270
+ if (N === 0) { return; }
1271
+
1272
+ const radius = 210; // orbit radius
1273
+ const cx = 240;
1274
+ const cy = 240;
1275
+
1276
+ for (let i = 0; i < N; i++) {
1277
+ const c = this.cosmoses[i];
1278
+ const angle = (i / N) * 2 * Math.PI - Math.PI / 2;
1279
+ const x = Math.round(cx + Math.cos(angle) * radius);
1280
+ const y = Math.round(cy + Math.sin(angle) * radius);
1281
+
1282
+ const btn = document.createElement('button');
1283
+ btn.type = 'button';
1284
+ btn.className = `orbit-cosmos-node ${c.id === this.activeCosmosId ? 'is-active' : ''}`;
1285
+ btn.style.left = `${x}px`;
1286
+ btn.style.top = `${y}px`;
1287
+ btn.title = `Enter ${c.name} (${c.spaces.length} spaces)`;
1288
+
1289
+ btn.innerHTML = `
1290
+ <span class="orbit-cosmos-icon">🌌</span>
1291
+ <span class="orbit-cosmos-name">${c.name}</span>
1292
+ <span class="orbit-cosmos-badge">${c.spaces.length} space${c.spaces.length === 1 ? '' : 's'}</span>
1293
+ `;
1294
+
1295
+ btn.addEventListener('click', (e) => {
1296
+ e.stopPropagation();
1297
+ this.selectCosmos(c.id);
1298
+ });
1299
+
1300
+ container.appendChild(btn);
1301
+ }
1302
+ }
1303
+
1304
+ public getCosmoses(): CosmosInfo[] {
1305
+ return [...this.cosmoses];
1306
+ }
1307
+
1308
+ public getActiveCosmos(): CosmosInfo | null {
1309
+ if (!this.activeCosmosId) return this.cosmoses[0] || null;
1310
+ return this.cosmoses.find(c => c.id === this.activeCosmosId) || this.cosmoses[0] || null;
1311
+ }
1312
+
1313
+ public getZoomState(): 'polycosmos' | 'cosmos' {
1314
+ return this.zoomState;
1315
+ }
1316
+ }
1317
+