@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,643 @@
1
+ import thisCss from "./code-editor.css";
2
+ import thisHtml from "./code-editor.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 { ElementsBase, IbGibDynamicComponentInstance } from "@ibgib/web-gib/dist/ui/component/component-types.mjs";
12
+ import { getComponentCtorArg } from "@ibgib/web-gib/dist/app-bootstrap/init-orchestration.mjs";
13
+ import { getDeterministicColorInfo } from "@ibgib/web-gib/dist/helpers.mjs";
14
+ import { GibColorInfo } from "@ibgib/web-gib/dist/types.mjs";
15
+ import { getIbGibFromSpace, getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
16
+ import { getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib } from "@ibgib/core-gib/dist/cosmology/branching/item/item-helpers.mjs";
17
+ import { B2tFSGalaxyIbGib_V1 } from "@ibgib/core-gib/dist/cosmology/branching/repo/b2tfs-repo-types.mjs";
18
+ import { getPlaygroundMetaspaceAndSpace } from "../../../cosmos/seed-cosmos-workspace.mjs";
19
+ import { getSpaceGibRouterSvc } from "../../router/index.mjs";
20
+
21
+ export const CODE_EDITOR_COMPONENT_NAME = 'space-gib-code-editor';
22
+
23
+ /**
24
+ * Metadata factory for space-gib-code-editor Web Component.
25
+ */
26
+ export class CodeEditorComponentMeta extends IbGibDynamicComponentMetaBase {
27
+ protected lc: string = `[${CodeEditorComponentMeta.name}]`;
28
+
29
+ routeRegExp?: RegExp = new RegExp(`^${CODE_EDITOR_COMPONENT_NAME}$`);
30
+ componentName = CODE_EDITOR_COMPONENT_NAME;
31
+
32
+ constructor() {
33
+ super(getComponentCtorArg());
34
+ if (!customElements.get(this.componentName)) {
35
+ customElements.define(this.componentName, CodeEditorComponentInstance);
36
+ }
37
+ }
38
+
39
+ async createInstance({
40
+ path,
41
+ ibGibAddr
42
+ }: {
43
+ path: string;
44
+ ibGibAddr: IbGibAddr;
45
+ }): Promise<IbGibDynamicComponentInstance> {
46
+ const component = document.createElement(this.componentName) as CodeEditorComponentInstance;
47
+ await component.initialize({
48
+ ibGibAddr: ibGibAddr || ROOT_ADDR,
49
+ meta: this,
50
+ html: thisHtml,
51
+ css: [thisCss],
52
+ });
53
+ return component;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * References to DOM elements within the code editor Shadow Root.
59
+ */
60
+ export interface CodeEditorElements extends ElementsBase {
61
+ rootEl: HTMLDivElement;
62
+ placeholderEl?: HTMLDivElement;
63
+ mainContainerEl?: HTMLDivElement;
64
+ headerEl: HTMLElement;
65
+ btnSaveSpinnerEl?: HTMLSpanElement;
66
+ crumbRepoEl: HTMLSpanElement;
67
+ crumbPathEl: HTMLSpanElement;
68
+ crumbBranchEl: HTMLSpanElement;
69
+ crumbTipEl: HTMLSpanElement;
70
+ crumbTipContainerEl: HTMLDivElement;
71
+ statLangEl: HTMLSpanElement;
72
+ statLinesEl: HTMLSpanElement;
73
+ statSizeEl: HTMLSpanElement;
74
+ btnToggleReadonlyEl: HTMLButtonElement;
75
+ modeIconEl: HTMLSpanElement;
76
+ modeTextEl: HTMLSpanElement;
77
+ btnCopyCodeEl: HTMLButtonElement;
78
+ editorGutterEl: HTMLDivElement;
79
+ editorTextareaEl: HTMLTextAreaElement;
80
+ contentEl: HTMLDivElement;
81
+ }
82
+
83
+ /**
84
+ * Concrete implementation of the space-gib-code-editor component.
85
+ */
86
+ export class CodeEditorComponentInstance
87
+ extends IbGibDynamicComponentInstanceBase<IbGib_V1, CodeEditorElements>
88
+ implements IbGibDynamicComponentInstance<IbGib_V1, CodeEditorElements> {
89
+
90
+ protected lc: string = `[${CodeEditorComponentInstance.name}]`;
91
+
92
+ private _readOnly: boolean = false;
93
+ private filepath: string = '';
94
+ private repo: string = '';
95
+ private branch: string = 'main';
96
+ private tipAddr: string = '';
97
+ private language: string = '';
98
+ private originalContent?: string;
99
+ private isUntracked: boolean = false;
100
+ private debounceTimer?: any;
101
+ private saveStartTime: number = 0;
102
+ private isSaving: boolean = false;
103
+ private branchColorCache = new Map<string, GibColorInfo>();
104
+
105
+ get el(): CodeEditorElements {
106
+ if (!this.elements) {
107
+ throw new Error(`elements is not defined in ${this.lc}`);
108
+ }
109
+ return this.elements;
110
+ }
111
+
112
+ /**
113
+ * Read-only mode flag. When true, the editor acts as a protected code viewer.
114
+ */
115
+ get readOnly(): boolean {
116
+ return this._readOnly;
117
+ }
118
+
119
+ set readOnly(val: boolean) {
120
+ this._readOnly = Boolean(val);
121
+ this.updateReadOnlyState();
122
+ }
123
+
124
+ /** Alias for readOnly */
125
+ get readonly(): boolean {
126
+ return this.readOnly;
127
+ }
128
+
129
+ set readonly(val: boolean) {
130
+ this.readOnly = val;
131
+ }
132
+
133
+ static get observedAttributes(): string[] {
134
+ return ['readonly'];
135
+ }
136
+
137
+ override async attributeChangedCallback(name: string, oldValue: any, newValue: any): Promise<void> {
138
+ await super.attributeChangedCallback(name, oldValue, newValue);
139
+ if (name === 'readonly') {
140
+ this.readOnly = newValue !== null && newValue !== 'false';
141
+ }
142
+ }
143
+
144
+ override async created(): Promise<void> {
145
+ const lc = `${this.lc}[${this.created.name}]`;
146
+ try {
147
+ const root = this.shadowRoot!;
148
+ this.elements = {
149
+ rootEl: root.getElementById('editor-root') as HTMLDivElement,
150
+ placeholderEl: root.getElementById('editor-empty-placeholder') as HTMLDivElement,
151
+ mainContainerEl: root.getElementById('editor-main-container') as HTMLDivElement,
152
+ headerEl: root.getElementById('editor-header') as HTMLElement,
153
+ crumbRepoEl: root.getElementById('crumb-repo') as HTMLSpanElement,
154
+ crumbPathEl: root.getElementById('crumb-path') as HTMLSpanElement,
155
+ crumbBranchEl: root.getElementById('crumb-branch') as HTMLSpanElement,
156
+ crumbTipEl: root.getElementById('crumb-tip') as HTMLSpanElement,
157
+ crumbTipContainerEl: root.getElementById('crumb-tip-container') as HTMLDivElement,
158
+ statLangEl: root.getElementById('stat-lang') as HTMLSpanElement,
159
+ statLinesEl: root.getElementById('stat-lines') as HTMLSpanElement,
160
+ statSizeEl: root.getElementById('stat-size') as HTMLSpanElement,
161
+ btnToggleReadonlyEl: root.getElementById('btn-toggle-readonly') as HTMLButtonElement,
162
+ btnSaveSpinnerEl: root.getElementById('btn-save-spinner') as HTMLSpanElement,
163
+ modeIconEl: root.getElementById('mode-icon') as HTMLSpanElement,
164
+ modeTextEl: root.getElementById('mode-text') as HTMLSpanElement,
165
+ btnCopyCodeEl: root.getElementById('btn-copy-code') as HTMLButtonElement,
166
+ editorGutterEl: root.getElementById('editor-gutter') as HTMLDivElement,
167
+ editorTextareaEl: root.getElementById('editor-textarea') as HTMLTextAreaElement,
168
+ contentEl: root.getElementById('editor-root') as HTMLDivElement,
169
+ };
170
+
171
+ // Sync readonly attribute if set on host
172
+ if (this.hasAttribute('readonly')) {
173
+ this._readOnly = true;
174
+ }
175
+
176
+ if (!this.filepath) {
177
+ this.clearFile({ repo: this.repo, branch: this.branch });
178
+ } else {
179
+ this.updateReadOnlyState();
180
+ this.updateGutter();
181
+ this.updateStats();
182
+ this.updateBranchColorStyling();
183
+ }
184
+
185
+ this.bindEvents();
186
+
187
+ } catch (error) {
188
+ console.error(`${lc} ${extractErrorMsg(error)}`);
189
+ }
190
+ }
191
+
192
+ private bindEvents(): void {
193
+ const lc = `${this.lc}[${this.bindEvents.name}]`;
194
+ if (!this.el) return;
195
+
196
+ // Textarea input: update line numbers & stats, reset 500ms debounce
197
+ this.el.editorTextareaEl?.addEventListener('input', () => {
198
+ this.updateGutter();
199
+ this.updateStats();
200
+
201
+ if (this.debounceTimer) {
202
+ clearTimeout(this.debounceTimer);
203
+ }
204
+ this.debounceTimer = setTimeout(() => {
205
+ this.saveStartTime = Date.now();
206
+ this.showSavingIndicator();
207
+ this.handleDebouncedSave();
208
+ }, 500);
209
+ });
210
+
211
+ // Synchronize scroll between textarea and line number gutter
212
+ this.el.editorTextareaEl?.addEventListener('scroll', () => {
213
+ if (this.el.editorGutterEl && this.el.editorTextareaEl) {
214
+ this.el.editorGutterEl.scrollTop = this.el.editorTextareaEl.scrollTop;
215
+ }
216
+ });
217
+
218
+ // Handle Tab key in textarea for indentation
219
+ this.el.editorTextareaEl?.addEventListener('keydown', (e: KeyboardEvent) => {
220
+ if (this._readOnly) return;
221
+
222
+ if (e.key === 'Tab') {
223
+ e.preventDefault();
224
+ const textarea = this.el.editorTextareaEl;
225
+ const start = textarea.selectionStart;
226
+ const end = textarea.selectionEnd;
227
+
228
+ // Insert 4 spaces
229
+ textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
230
+ textarea.selectionStart = textarea.selectionEnd = start + 4;
231
+
232
+ this.updateGutter();
233
+ this.updateStats();
234
+ }
235
+ });
236
+
237
+ // Toggle Read-Only / Editable Mode
238
+ this.el.btnToggleReadonlyEl?.addEventListener('click', () => {
239
+ this.readOnly = !this.readOnly;
240
+ });
241
+
242
+ // Copy Code Button
243
+ this.el.btnCopyCodeEl?.addEventListener('click', async () => {
244
+ const code = this.getContent();
245
+ try {
246
+ if (navigator.clipboard) {
247
+ await navigator.clipboard.writeText(code);
248
+ } else {
249
+ // Fallback
250
+ const ta = document.createElement('textarea');
251
+ ta.value = code;
252
+ document.body.appendChild(ta);
253
+ ta.select();
254
+ document.execCommand('copy');
255
+ document.body.removeChild(ta);
256
+ }
257
+
258
+ if (this.el.btnCopyCodeEl) {
259
+ const originalHtml = this.el.btnCopyCodeEl.innerHTML;
260
+ this.el.btnCopyCodeEl.innerHTML = '<span class="btn-icon">✓</span>';
261
+ setTimeout(() => {
262
+ if (this.el?.btnCopyCodeEl) {
263
+ this.el.btnCopyCodeEl.innerHTML = originalHtml;
264
+ }
265
+ }, 1500);
266
+ }
267
+
268
+ this.dispatchEvent(new CustomEvent('space-gib-code-copied', {
269
+ bubbles: true,
270
+ composed: true,
271
+ detail: { filepath: this.filepath }
272
+ }));
273
+ } catch (err) {
274
+ console.warn(`${lc} Failed to copy code: ${extractErrorMsg(err)}`);
275
+ }
276
+ });
277
+ }
278
+
279
+ private updateReadOnlyState(): void {
280
+ if (!this.elements) return;
281
+
282
+ if (this.el.editorTextareaEl) {
283
+ this.el.editorTextareaEl.readOnly = this._readOnly;
284
+ }
285
+
286
+ if (this.el.rootEl) {
287
+ this.el.rootEl.classList.toggle('readonly-mode', this._readOnly);
288
+ this.el.rootEl.classList.toggle('editable-mode', !this._readOnly);
289
+ }
290
+
291
+ if (this.el.btnToggleReadonlyEl) {
292
+ this.el.btnToggleReadonlyEl.classList.toggle('readonly', this._readOnly);
293
+ this.el.btnToggleReadonlyEl.classList.toggle('editable', !this._readOnly);
294
+ if (!this.isSaving) {
295
+ this.el.btnToggleReadonlyEl.classList.remove('saving');
296
+ }
297
+ }
298
+
299
+ if (!this.isSaving) {
300
+ if (this.el.btnSaveSpinnerEl) {
301
+ this.el.btnSaveSpinnerEl.style.display = 'none';
302
+ }
303
+ if (this.el.modeIconEl) {
304
+ this.el.modeIconEl.style.display = 'inline';
305
+ this.el.modeIconEl.textContent = this._readOnly ? '🔒' : '✏️';
306
+ }
307
+ if (this.el.modeTextEl) {
308
+ this.el.modeTextEl.textContent = this._readOnly ? 'Read-Only' : 'Editable';
309
+ }
310
+ }
311
+ }
312
+
313
+ private showSavingIndicator(): void {
314
+ this.isSaving = true;
315
+ if (this.elements) {
316
+ if (this.el.btnSaveSpinnerEl) {
317
+ this.el.btnSaveSpinnerEl.style.display = 'inline-block';
318
+ }
319
+ if (this.el.modeIconEl) {
320
+ this.el.modeIconEl.style.display = 'none';
321
+ }
322
+ if (this.el.modeTextEl) {
323
+ this.el.modeTextEl.textContent = 'Saving...';
324
+ }
325
+ if (this.el.btnToggleReadonlyEl) {
326
+ this.el.btnToggleReadonlyEl.classList.add('saving');
327
+ }
328
+ }
329
+ }
330
+
331
+ private hideSavingIndicator(): void {
332
+ this.isSaving = false;
333
+ if (this.elements) {
334
+ if (this.el.btnSaveSpinnerEl) {
335
+ this.el.btnSaveSpinnerEl.style.display = 'none';
336
+ }
337
+ if (this.el.modeIconEl) {
338
+ this.el.modeIconEl.style.display = 'inline';
339
+ }
340
+ if (this.el.btnToggleReadonlyEl) {
341
+ this.el.btnToggleReadonlyEl.classList.remove('saving');
342
+ }
343
+ this.updateReadOnlyState();
344
+ }
345
+ }
346
+
347
+ private async handleDebouncedSave(): Promise<void> {
348
+ if (!this.filepath) return;
349
+ const currentContent = this.getContent();
350
+
351
+ // Diff against originalContent
352
+ const isReverted = !this.isUntracked && this.originalContent !== undefined && currentContent === this.originalContent;
353
+
354
+ this.dispatchEvent(new CustomEvent('space-gib-code-changed', {
355
+ bubbles: true,
356
+ composed: true,
357
+ detail: {
358
+ filepath: this.filepath,
359
+ content: currentContent,
360
+ lineCount: this.getLineCount(),
361
+ isReverted,
362
+ isUntracked: this.isUntracked,
363
+ repo: this.repo,
364
+ branch: this.branch,
365
+ }
366
+ }));
367
+
368
+ // Minimum 0.5s (500ms) animation duration for saving indicator
369
+ const elapsed = Date.now() - this.saveStartTime;
370
+ const remaining = Math.max(0, 500 - elapsed);
371
+ setTimeout(() => {
372
+ this.hideSavingIndicator();
373
+ this.saveStartTime = 0;
374
+ }, remaining);
375
+ }
376
+
377
+ private getLineCount(): number {
378
+ const text = this.el?.editorTextareaEl?.value || '';
379
+ return text.split('\n').length;
380
+ }
381
+
382
+ private updateGutter(): void {
383
+ if (!this.el?.editorGutterEl) return;
384
+ const lineCount = this.getLineCount();
385
+ const linesHtml = Array.from({ length: lineCount }, (_, i) => `<span class="line-num">${i + 1}</span>`).join('');
386
+ this.el.editorGutterEl.innerHTML = linesHtml;
387
+ }
388
+
389
+ private updateStats(): void {
390
+ if (!this.elements) return;
391
+ const text = this.el.editorTextareaEl?.value || '';
392
+ const lines = text.split('\n').length;
393
+ const bytes = new Blob([text]).size;
394
+
395
+ if (this.el.statLinesEl) {
396
+ this.el.statLinesEl.textContent = `${lines} line${lines === 1 ? '' : 's'}`;
397
+ }
398
+ if (this.el.statSizeEl) {
399
+ this.el.statSizeEl.textContent = bytes > 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${bytes} B`;
400
+ }
401
+ if (this.el.statLangEl) {
402
+ this.el.statLangEl.textContent = this.language;
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Loads a file into the editor, updating breadcrumbs, content, branch tip, and stats.
408
+ */
409
+ public loadFile(opts: {
410
+ filepath: string;
411
+ content?: string;
412
+ originalContent?: string;
413
+ isUntracked?: boolean;
414
+ branch?: string;
415
+ tipAddr?: string;
416
+ language?: string;
417
+ repo?: string;
418
+ readOnly?: boolean;
419
+ }): void {
420
+ if (this.debounceTimer) {
421
+ clearTimeout(this.debounceTimer);
422
+ this.debounceTimer = undefined;
423
+ }
424
+ this.saveStartTime = 0;
425
+ this.hideSavingIndicator();
426
+
427
+ this.filepath = opts.filepath || this.filepath;
428
+ if (opts.repo) this.repo = opts.repo;
429
+ if (opts.branch) this.branch = opts.branch;
430
+ if (opts.tipAddr) this.tipAddr = opts.tipAddr;
431
+
432
+ if (opts.originalContent !== undefined) {
433
+ this.originalContent = opts.originalContent;
434
+ } else if (opts.content !== undefined && !opts.isUntracked) {
435
+ this.originalContent = opts.content;
436
+ } else {
437
+ this.originalContent = undefined;
438
+ }
439
+ this.isUntracked = Boolean(opts.isUntracked);
440
+
441
+ // Auto-detect language from extension if not provided
442
+ if (opts.language) {
443
+ this.language = opts.language;
444
+ } else {
445
+ const ext = this.filepath.split('.').pop()?.toLowerCase();
446
+ switch (ext) {
447
+ case 'ts':
448
+ case 'mts':
449
+ this.language = 'TypeScript';
450
+ break;
451
+ case 'js':
452
+ case 'mjs':
453
+ this.language = 'JavaScript';
454
+ break;
455
+ case 'html':
456
+ this.language = 'HTML';
457
+ break;
458
+ case 'css':
459
+ this.language = 'CSS';
460
+ break;
461
+ case 'json':
462
+ this.language = 'JSON';
463
+ break;
464
+ case 'md':
465
+ this.language = 'Markdown';
466
+ break;
467
+ default:
468
+ this.language = 'Plain Text';
469
+ }
470
+ }
471
+
472
+ if (opts.readOnly !== undefined) {
473
+ this.readOnly = opts.readOnly;
474
+ }
475
+
476
+ if (this.elements) {
477
+ if (this.el.placeholderEl) this.el.placeholderEl.style.display = 'none';
478
+ if (this.el.mainContainerEl) this.el.mainContainerEl.style.display = 'flex';
479
+
480
+ if (this.el.crumbRepoEl) this.el.crumbRepoEl.textContent = this.repo;
481
+ if (this.el.crumbPathEl) this.el.crumbPathEl.textContent = this.filepath;
482
+ if (this.el.crumbBranchEl) this.el.crumbBranchEl.textContent = this.branch;
483
+ if (this.el.crumbTipEl) this.el.crumbTipEl.textContent = this.tipAddr;
484
+
485
+ if (opts.content !== undefined && this.el.editorTextareaEl) {
486
+ this.el.editorTextareaEl.value = opts.content;
487
+ }
488
+
489
+ this.updateReadOnlyState();
490
+ this.updateGutter();
491
+ this.updateStats();
492
+ }
493
+ this.updateBranchColorStyling();
494
+ }
495
+
496
+ public setTipAddr(tipAddr: string): void {
497
+ this.tipAddr = tipAddr;
498
+ if (this.elements && this.el.crumbTipEl) {
499
+ this.el.crumbTipEl.textContent = tipAddr;
500
+ }
501
+ }
502
+
503
+ public markAsCommitted(tipAddr?: string): void {
504
+ this.originalContent = this.getContent();
505
+ this.isUntracked = false;
506
+ if (tipAddr) {
507
+ this.setTipAddr(tipAddr);
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Clears the active file and displays the empty state placeholder.
513
+ */
514
+ public clearFile(opts?: { repo?: string; branch?: string }): void {
515
+ if (this.debounceTimer) {
516
+ clearTimeout(this.debounceTimer);
517
+ this.debounceTimer = undefined;
518
+ }
519
+ this.saveStartTime = 0;
520
+ this.hideSavingIndicator();
521
+ this.originalContent = undefined;
522
+ this.isUntracked = false;
523
+
524
+ this.filepath = '';
525
+ if (opts?.repo) this.repo = opts.repo;
526
+ if (opts?.branch) this.branch = opts.branch;
527
+ this.tipAddr = '';
528
+ this.language = '';
529
+
530
+ if (this.elements) {
531
+ if (this.el.placeholderEl) this.el.placeholderEl.style.display = 'flex';
532
+ if (this.el.mainContainerEl) this.el.mainContainerEl.style.display = 'none';
533
+
534
+ if (this.el.crumbRepoEl) this.el.crumbRepoEl.textContent = this.repo;
535
+ if (this.el.crumbPathEl) this.el.crumbPathEl.textContent = '';
536
+ if (this.el.crumbBranchEl) this.el.crumbBranchEl.textContent = this.branch;
537
+ if (this.el.crumbTipEl) this.el.crumbTipEl.textContent = '';
538
+
539
+ if (this.el.editorTextareaEl) {
540
+ this.el.editorTextareaEl.value = '';
541
+ }
542
+
543
+ this.updateReadOnlyState();
544
+ this.updateGutter();
545
+ this.updateStats();
546
+ }
547
+ this.updateBranchColorStyling();
548
+ }
549
+
550
+ public getFilepath(): string {
551
+ return this.filepath;
552
+ }
553
+
554
+ public getContent(): string {
555
+ return this.el?.editorTextareaEl?.value || '';
556
+ }
557
+
558
+ public setContent(content: string): void {
559
+ if (this.el?.editorTextareaEl) {
560
+ this.el.editorTextareaEl.value = content;
561
+ this.updateGutter();
562
+ this.updateStats();
563
+ }
564
+ }
565
+
566
+ public setReadOnly(readOnly: boolean): void {
567
+ this.readOnly = readOnly;
568
+ }
569
+
570
+ public setGalaxyAndBranch(repo: string, branch: string = 'main'): void {
571
+ this.repo = repo;
572
+ this.branch = branch;
573
+ if (this.elements) {
574
+ if (this.el.crumbRepoEl) this.el.crumbRepoEl.textContent = this.repo;
575
+ if (this.el.crumbBranchEl) this.el.crumbBranchEl.textContent = this.branch;
576
+ }
577
+ this.updateBranchColorStyling();
578
+ }
579
+
580
+ public clearBranchColorCache(): void {
581
+ this.branchColorCache.clear();
582
+ }
583
+
584
+ private async resolveGalaxyIbGib(galaxyName: string, space: any): Promise<B2tFSGalaxyIbGib_V1 | undefined> {
585
+ const router = getSpaceGibRouterSvc();
586
+ for (const manifest of router.cosmosManifestCache?.values() || []) {
587
+ const entry = manifest.data?.galaxies?.[galaxyName];
588
+ if (entry?.galaxyAddr) {
589
+ const gib = await getIbGibFromSpace<B2tFSGalaxyIbGib_V1>({ addr: entry.galaxyAddr, space });
590
+ if (gib) return gib;
591
+ }
592
+ }
593
+ return undefined;
594
+ }
595
+
596
+ public async updateBranchColorStyling(): Promise<void> {
597
+ try {
598
+ if (!this.repo) return;
599
+ const cacheKey = `${this.repo || 'unknown'}~${this.branch}`;
600
+ let colorInfo = this.branchColorCache.get(cacheKey);
601
+ if (!colorInfo) {
602
+ const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
603
+ const galaxyIbGib = await this.resolveGalaxyIbGib(this.repo, space);
604
+ let branchAddrOrTjp: string | undefined;
605
+ if (galaxyIbGib) {
606
+ const branchAndRoot = await getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib({
607
+ galaxyIbGib,
608
+ branchName: this.branch,
609
+ metaspace,
610
+ }).catch(() => undefined);
611
+ if (branchAndRoot?.branchIbGib) {
612
+ branchAddrOrTjp = getTjpAddr({ ibGib: branchAndRoot.branchIbGib }) || branchAndRoot.branchInfo?.branchAddr;
613
+ }
614
+ }
615
+ colorInfo = getDeterministicColorInfo({
616
+ ibGibAddr: branchAddrOrTjp,
617
+ gib: branchAddrOrTjp ? undefined : this.branch,
618
+ translucentAlpha: 18,
619
+ });
620
+ if (this.repo && branchAddrOrTjp) {
621
+ this.branchColorCache.set(cacheKey, colorInfo);
622
+ }
623
+ }
624
+
625
+ if (colorInfo) {
626
+ this.style.setProperty('--branch-accent-color', colorInfo.punctiliarColor);
627
+ this.style.setProperty('--branch-accent-translucent', colorInfo.punctiliarColorTranslucent);
628
+ this.style.setProperty('--branch-accent-contrast', colorInfo.punctiliarColorContrast);
629
+ }
630
+ } catch (err) {
631
+ console.warn(`${this.lc} Error updating branch color styling: ${extractErrorMsg(err)}`);
632
+ }
633
+ }
634
+
635
+ override async disconnected(): Promise<void> {
636
+ const lc = `${this.lc}[${this.disconnected.name}]`;
637
+ try {
638
+ // cleanup if needed
639
+ } catch (error) {
640
+ console.error(`${lc} ${extractErrorMsg(error)}`);
641
+ }
642
+ }
643
+ }
@@ -0,0 +1,9 @@
1
+ import { getComponentSvc } from "@ibgib/web-gib/dist/ui/component/ibgib-component-service.mjs";
2
+ import { CodeEditorComponentMeta } from "./code-editor.mjs";
3
+
4
+ export * from "./code-editor.mjs";
5
+
6
+ export async function registerCodeEditorComponent(): Promise<void> {
7
+ const componentSvc = await getComponentSvc();
8
+ componentSvc.registerComponentMeta(new CodeEditorComponentMeta());
9
+ }
@@ -0,0 +1,31 @@
1
+ # space-gib-cosmos-navigator Implementation Specification
2
+
3
+ ## Overview
4
+ `space-gib-cosmos-navigator` is a custom Web Component built on top of the `@ibgib/web-gib` dynamic component microframework. It acts as the core cosmological navigation interface for `space-gib`, presenting both the overarching **Polycosmos** (multi-project) zoom state and the active **Cosmos** (project) zoom state within a single unified component.
5
+
6
+ ## Architecture & Lifecycles
7
+ - **Tag Name**: `space-gib-cosmos-navigator`
8
+ - **Component Meta**: `CosmosNavigatorComponentMeta` (extends `IbGibDynamicComponentMetaBase`)
9
+ - **Component Instance**: `CosmosNavigatorComponentInstance` (extends `IbGibDynamicComponentInstanceBase`)
10
+ - **Backing ibGib**: Initialized with `ROOT_ADDR` (virtual root genesis).
11
+ - **Shadow DOM**: Uses open Shadow DOM. Encapsulates the starfield particle canvas, the central Big Bang singularity, orbiting cosmos nodes, and active cosmos cluster satellites.
12
+ - **Dynamic Injection**: Injected by `SpaceGibShellService` into host `<div id="cosmos-navigator-container"></div>` via `IbGibComponentService`.
13
+
14
+ ## State & Zoom Continuum
15
+ 1. **Empty Primordial State (0 Cosmoses)**:
16
+ - Central Big Bang singularity is visible.
17
+ - Clicking it triggers the Big Bang explosion and creates Cosmos 1.
18
+ 2. **Polycosmos State (Zoomed Out)**:
19
+ - Central Big Bang singularity acts as the "+ New Cosmos" trigger.
20
+ - Existing cosmoses orbit around it as glowing galaxy nodes with space count badges.
21
+ - Clicking an orbiting cosmos calls `selectCosmos(id)` to zoom in.
22
+ - Clicking the central Big Bang creates a new Cosmos (orbiting nodes disperse, explosion fires, new Cosmos N is born).
23
+ 3. **Cosmos State (Zoomed In)**:
24
+ - Focuses on the selected active cosmos.
25
+ - Central Superspaces galaxy node unfolds its satellites (Code Space `</>`, Text Space `📝`).
26
+ - Clicking a space triggers the app launch animation sequence and adds it to the Activity Bar rail.
27
+
28
+ ## Custom Events Emitted (Composed & Bubbling)
29
+ - `space-gib-cosmos-selected`: Emitted when a cosmos is selected (detail: `{ cosmosId, name, spaces }`).
30
+ - `space-gib-space-added`: Emitted when a space (code/text) is added to the active cosmos (detail: `{ cosmosId, space }`).
31
+ - `space-gib-polycosmos-activated`: Emitted when navigating out to the Polycosmos level.