@ibgib/space-gib 0.0.32 → 0.0.34

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