@ibgib/space-gib 0.0.33 → 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 +32 -32
  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-CRJV762I.mjs +0 -3102
  111. package/dist/client/chunk-LXRCF5OS.mjs +0 -25
  112. package/dist/client/chunk-UBDQUZZV.mjs +0 -1
@@ -0,0 +1,646 @@
1
+ import thisCss from "./chat.css";
2
+ import thisHtml from "./chat.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 { getIbAndGib } from "@ibgib/ts-gib/dist/helper.mjs";
8
+ import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
9
+ import {
10
+ IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase,
11
+ } from "@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs";
12
+ import {
13
+ ElementsBase, IbGibDynamicComponentInstance,
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
+
17
+ import {
18
+ CHAT_COMPONENT_NAME, EVENT_SPACE_GIB_CHAT_DRILL_DOWN,
19
+ } from "../nested-chat-constants.mjs";
20
+ import {
21
+ ChatCommentItem, ChatDrillDownEventDetail,
22
+ } from "../nested-chat-types.mjs";
23
+ import { getSavedEnterBehavior, setSavedEnterBehavior } from "./chat-helpers.mjs";
24
+
25
+ /**
26
+ * Metadata factory for space-gib-chat Web Component.
27
+ */
28
+ export class ChatComponentMeta extends IbGibDynamicComponentMetaBase {
29
+ protected lc: string = `[${ChatComponentMeta.name}]`;
30
+
31
+ routeRegExp?: RegExp = new RegExp(`^${CHAT_COMPONENT_NAME}$`);
32
+ componentName = CHAT_COMPONENT_NAME;
33
+
34
+ constructor() {
35
+ super(getComponentCtorArg());
36
+ if (!customElements.get(this.componentName)) {
37
+ customElements.define(this.componentName, ChatComponentInstance);
38
+ }
39
+ }
40
+
41
+ async createInstance({
42
+ path,
43
+ ibGibAddr
44
+ }: {
45
+ path: string;
46
+ ibGibAddr: IbGibAddr;
47
+ }): Promise<IbGibDynamicComponentInstance> {
48
+ const component = document.createElement(this.componentName) as ChatComponentInstance;
49
+ await component.initialize({
50
+ ibGibAddr: ibGibAddr || ROOT_ADDR,
51
+ meta: this,
52
+ html: thisHtml,
53
+ css: [thisCss],
54
+ });
55
+ return component;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * References to DOM elements within the component's Shadow Root.
61
+ */
62
+ export interface ChatElements extends ElementsBase {
63
+ rootEl: HTMLDivElement;
64
+ contextTitleEl: HTMLSpanElement;
65
+ messageCountEl: HTMLSpanElement;
66
+ messagesContainerEl: HTMLDivElement;
67
+ emptyStateEl: HTMLDivElement;
68
+ messagesListEl: HTMLDivElement;
69
+ inputEl: HTMLTextAreaElement;
70
+ btnSendEl: HTMLButtonElement;
71
+ contentEl: HTMLDivElement;
72
+ titleInputEl: HTMLInputElement;
73
+ btnToggleEncryptEl: HTMLButtonElement;
74
+ btnTogglePreviewEl: HTMLButtonElement;
75
+ btnToggleCollapseEl: HTMLButtonElement;
76
+ composeBodyEl: HTMLDivElement;
77
+ composeCollapseArrowEl: HTMLSpanElement;
78
+ previewDrawerEl: HTMLDivElement;
79
+ previewTitleRenderEl: HTMLHeadingElement;
80
+ previewBodyRenderEl: HTMLDivElement;
81
+ previewEncryptionNoticeEl: HTMLSpanElement;
82
+ encryptDrawerEl: HTMLDivElement;
83
+ sessionKeyBadgeEl: HTMLSpanElement;
84
+ inputPassphraseEl: HTMLInputElement;
85
+ selectAlgoEl: HTMLSelectElement;
86
+ textareaPreviewEl: HTMLTextAreaElement;
87
+ selectEnterBehaviorEl: HTMLSelectElement;
88
+ }
89
+
90
+ /**
91
+ * Concrete implementation of a single contextual chatroom view in space-gib.
92
+ * In the ibGib model, any ibGib (file, commit, or comment) acts as a chatroom context.
93
+ */
94
+ export class ChatComponentInstance
95
+ extends IbGibDynamicComponentInstanceBase<IbGib_V1, ChatElements>
96
+ implements IbGibDynamicComponentInstance<IbGib_V1, ChatElements> {
97
+
98
+ protected lc: string = `[${ChatComponentInstance.name}]`;
99
+
100
+ get el(): ChatElements {
101
+ if (!this.elements) {
102
+ throw new Error(`elements is not defined in ${this.lc}`);
103
+ }
104
+ return this.elements;
105
+ }
106
+
107
+ private messages: ChatCommentItem[] = [];
108
+ private static sessionPassphrase: string = '';
109
+ private isEncryptActive: boolean = false;
110
+ private isPreviewActive: boolean = false;
111
+ private isComposeCollapsed: boolean = false;
112
+ private isNewTopicMode: boolean = false;
113
+ private previewDebounceTimer: any = null;
114
+
115
+ override async created(): Promise<void> {
116
+ const lc = `${this.lc}[${this.created.name}]`;
117
+ try {
118
+ const root = this.shadowRoot!;
119
+ this.elements = {
120
+ rootEl: root.getElementById('chat-child-root') as HTMLDivElement,
121
+ contextTitleEl: root.getElementById('chat-context-title') as HTMLSpanElement,
122
+ messageCountEl: root.getElementById('chat-message-count') as HTMLSpanElement,
123
+ messagesContainerEl: root.getElementById('chat-messages-container') as HTMLDivElement,
124
+ emptyStateEl: root.getElementById('chat-empty-state') as HTMLDivElement,
125
+ messagesListEl: root.getElementById('chat-messages-list') as HTMLDivElement,
126
+ inputEl: root.getElementById('chat-input') as HTMLTextAreaElement,
127
+ btnSendEl: root.getElementById('btn-chat-send') as HTMLButtonElement,
128
+ contentEl: root.getElementById('chat-child-root') as HTMLDivElement,
129
+ titleInputEl: root.getElementById('chat-title-input') as HTMLInputElement,
130
+ btnToggleEncryptEl: root.getElementById('btn-toggle-encrypt') as HTMLButtonElement,
131
+ btnTogglePreviewEl: root.getElementById('btn-toggle-preview') as HTMLButtonElement,
132
+ btnToggleCollapseEl: root.getElementById('btn-toggle-compose-collapse') as HTMLButtonElement,
133
+ composeBodyEl: root.getElementById('chat-compose-body') as HTMLDivElement,
134
+ composeCollapseArrowEl: root.getElementById('compose-collapse-arrow') as HTMLSpanElement,
135
+ previewDrawerEl: root.getElementById('chat-preview-drawer') as HTMLDivElement,
136
+ previewTitleRenderEl: root.getElementById('preview-title-render') as HTMLHeadingElement,
137
+ previewBodyRenderEl: root.getElementById('preview-body-render') as HTMLDivElement,
138
+ previewEncryptionNoticeEl: root.getElementById('preview-encryption-notice') as HTMLSpanElement,
139
+ encryptDrawerEl: root.getElementById('chat-encrypt-drawer') as HTMLDivElement,
140
+ sessionKeyBadgeEl: root.getElementById('session-key-badge') as HTMLSpanElement,
141
+ inputPassphraseEl: root.getElementById('input-encrypt-passphrase') as HTMLInputElement,
142
+ selectAlgoEl: root.getElementById('select-encrypt-algo') as HTMLSelectElement,
143
+ textareaPreviewEl: root.getElementById('textarea-encrypt-preview') as HTMLTextAreaElement,
144
+ selectEnterBehaviorEl: root.getElementById('select-enter-behavior') as HTMLSelectElement,
145
+ };
146
+
147
+ this.initContextInfo();
148
+ this.loadInitialMessages();
149
+ await this.initEnterBehavior();
150
+ this.bindEvents();
151
+ this.renderMessages();
152
+
153
+ } catch (error) {
154
+ console.error(`${lc} ${extractErrorMsg(error)}`);
155
+ }
156
+ }
157
+
158
+ override async disconnected(): Promise<void> {
159
+ // Lifecycle teardown if required
160
+ }
161
+
162
+ private initContextInfo(): void {
163
+ if (!this.el?.contextTitleEl) return;
164
+
165
+ const addr = this.ibGibAddr ?? ROOT_ADDR;
166
+ const { ib } = getIbAndGib({ ibGibAddr: addr });
167
+
168
+ if (addr === ROOT_ADDR || ib === 'root' || ib.includes('space-gib')) {
169
+ this.el.contextTitleEl.textContent = 'Thread: src/index.ts';
170
+ } else if (ib.startsWith('comment')) {
171
+ const pieces = ib.split(' ');
172
+ const preview = pieces.length > 1 ? pieces.slice(1).join(' ') : 'Comment Thread';
173
+ this.el.contextTitleEl.textContent = `Thread: "${preview.slice(0, 24)}..."`;
174
+ } else {
175
+ this.el.contextTitleEl.textContent = `Thread: ${ib.slice(0, 20)}`;
176
+ }
177
+ }
178
+
179
+ private loadInitialMessages(): void {
180
+ const addr = this.ibGibAddr ?? ROOT_ADDR;
181
+ const { ib } = getIbAndGib({ ibGibAddr: addr });
182
+
183
+ if (ib.includes('boundary') || ib.includes('try/catch') || ib.includes('boot')) {
184
+ // Nested replies for the error boundary comment thread
185
+ this.messages = [
186
+ {
187
+ id: 'submsg-1',
188
+ addr: 'comment boundary-rationale^gib_b1',
189
+ author: 'Charlie',
190
+ avatar: '👩‍🔬',
191
+ role: 'Peer',
192
+ timestamp: '3m ago',
193
+ text: 'Wrapping boot() in a top-level error boundary prevents uncaught rejections from freezing the virtual canvas.',
194
+ replyCount: 1,
195
+ },
196
+ {
197
+ id: 'submsg-2',
198
+ addr: 'comment boundary-event^gib_b2',
199
+ author: 'Bob',
200
+ avatar: '👨‍🚀',
201
+ role: 'Peer',
202
+ timestamp: '1m ago',
203
+ text: 'Agreed! Let us also dispatch an EVENT_IBGIB_UI_MESSAGE so the status bar shows the error banner.',
204
+ replyCount: 0,
205
+ }
206
+ ];
207
+ } else if (ib.includes('rationale') || ib.includes('index.ts') || ib.includes('indexeddb')) {
208
+ // Nested replies for the architectural rationale comment thread
209
+ this.messages = [
210
+ {
211
+ id: 'submsg-3',
212
+ addr: 'comment offline-merkle^gib_a1',
213
+ author: 'Bob',
214
+ avatar: '👨‍🚀',
215
+ role: 'Peer',
216
+ timestamp: '5m ago',
217
+ text: 'The local IndexedDB space works completely offline without network roundtrips.',
218
+ replyCount: 0,
219
+ },
220
+ {
221
+ id: 'submsg-4',
222
+ addr: 'comment sync-keystone^gib_a2',
223
+ author: 'Alice',
224
+ avatar: '👩‍💻',
225
+ role: 'Keystone',
226
+ timestamp: '2m ago',
227
+ text: 'Exactly! Remote peer hubs synchronize automatically via WebSockets whenever connectivity is detected.',
228
+ replyCount: 0,
229
+ }
230
+ ];
231
+ } else {
232
+ // Default file / root comments
233
+ this.messages = [
234
+ {
235
+ id: 'msg-1',
236
+ addr: 'comment index-arch-rationale^gib_1',
237
+ author: 'Alice',
238
+ avatar: '👩‍💻',
239
+ role: 'Keystone',
240
+ timestamp: '10m ago',
241
+ anchor: 'src/index.ts#L1-18',
242
+ text: 'Architectural Rationale: This entrypoint initializes the virtual repository workspace within the browser’s local IndexedDB metaspace.',
243
+ replyCount: 2,
244
+ },
245
+ {
246
+ id: 'msg-2',
247
+ addr: 'comment index-try-catch^gib_2',
248
+ author: 'Bob',
249
+ avatar: '👨‍🚀',
250
+ role: 'Peer',
251
+ timestamp: '6m ago',
252
+ anchor: 'src/index.ts#L36-42',
253
+ text: 'Looks super clean! Should we add an async try/catch boundary around boot() as well?',
254
+ replyCount: 2,
255
+ },
256
+ {
257
+ id: 'msg-3',
258
+ addr: 'comment index-agent-analysis^gib_3',
259
+ author: 'Agent',
260
+ avatar: '🤖',
261
+ role: 'Agent',
262
+ timestamp: '4m ago',
263
+ anchor: 'src/index.ts#L43-50',
264
+ text: 'Static analysis confirms that initOrchestration provides a root failure boundary.',
265
+ replyCount: 0,
266
+ }
267
+ ];
268
+ }
269
+ }
270
+
271
+ private async initEnterBehavior(): Promise<void> {
272
+ if (!this.el.selectEnterBehaviorEl) return;
273
+ this.el.selectEnterBehaviorEl.value = await getSavedEnterBehavior();
274
+ this.updateInputPlaceholder();
275
+ }
276
+
277
+ private updateInputPlaceholder(): void {
278
+ if (!this.el?.inputEl) return;
279
+ const behavior = this.el.selectEnterBehaviorEl?.value || getSavedEnterBehavior();
280
+ if (behavior === 'send') {
281
+ this.el.inputEl.placeholder = 'Write markdown comment... (Shift+Enter for newline)';
282
+ } else {
283
+ this.el.inputEl.placeholder = 'Write markdown comment... (Ctrl+Enter to send)';
284
+ }
285
+ }
286
+
287
+ private bindEvents(): void {
288
+ if (!this.el) return;
289
+
290
+ this.el.btnSendEl?.addEventListener('click', () => this.sendMessage());
291
+
292
+ this.el.selectEnterBehaviorEl?.addEventListener('change', () => {
293
+ const val = (this.el.selectEnterBehaviorEl?.value === 'newline' ? 'newline' : 'send');
294
+ setSavedEnterBehavior(val);
295
+ this.updateInputPlaceholder();
296
+ });
297
+
298
+ this.el.inputEl?.addEventListener('keydown', (e: KeyboardEvent) => {
299
+ const behavior = this.el.selectEnterBehaviorEl?.value || getSavedEnterBehavior();
300
+ if (behavior === 'send') {
301
+ if (e.key === 'Enter' && !e.shiftKey) {
302
+ e.preventDefault();
303
+ this.sendMessage();
304
+ }
305
+ } else {
306
+ // Newline mode: Enter inserts a newline (default).
307
+ // Ctrl+Enter or Cmd+Enter sends the message.
308
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
309
+ e.preventDefault();
310
+ this.sendMessage();
311
+ }
312
+ }
313
+ });
314
+
315
+ this.el.titleInputEl?.addEventListener('keydown', (e: KeyboardEvent) => {
316
+ if (e.key === 'Enter') {
317
+ e.preventDefault();
318
+ this.el.inputEl?.focus();
319
+ }
320
+ });
321
+
322
+ // Title input & clear required alert
323
+ this.el.titleInputEl?.addEventListener('input', () => {
324
+ this.el.titleInputEl.classList.remove('required-alert');
325
+ this.updatePreviews();
326
+ });
327
+
328
+ // Auto-grow textarea & live preview debounce
329
+ this.el.inputEl?.addEventListener('input', () => {
330
+ if (this.el.inputEl) {
331
+ this.el.inputEl.style.height = 'auto';
332
+ this.el.inputEl.style.height = Math.min(this.el.inputEl.scrollHeight, 120) + 'px';
333
+ }
334
+ this.updatePreviews();
335
+ });
336
+
337
+ // Compose body collapse toggle
338
+ this.el.btnToggleCollapseEl?.addEventListener('click', () => {
339
+ this.isComposeCollapsed = !this.isComposeCollapsed;
340
+ this.el.composeBodyEl.classList.toggle('collapsed', this.isComposeCollapsed);
341
+ this.el.composeCollapseArrowEl.textContent = this.isComposeCollapsed ? '▲' : '▼';
342
+ });
343
+
344
+ // Encryption drawer toggle
345
+ this.el.btnToggleEncryptEl?.addEventListener('click', () => {
346
+ this.isEncryptActive = !this.isEncryptActive;
347
+ this.el.btnToggleEncryptEl.classList.toggle('active', this.isEncryptActive);
348
+ this.el.encryptDrawerEl.classList.toggle('hidden', !this.isEncryptActive);
349
+ if (this.isEncryptActive) {
350
+ if (ChatComponentInstance.sessionPassphrase && this.el.inputPassphraseEl) {
351
+ this.el.inputPassphraseEl.value = ChatComponentInstance.sessionPassphrase;
352
+ this.el.sessionKeyBadgeEl.classList.remove('hidden');
353
+ }
354
+ this.el.inputPassphraseEl?.focus();
355
+ }
356
+ this.updatePreviews();
357
+ });
358
+
359
+ // Preview drawer toggle
360
+ this.el.btnTogglePreviewEl?.addEventListener('click', () => {
361
+ this.isPreviewActive = !this.isPreviewActive;
362
+ this.el.btnTogglePreviewEl.classList.toggle('active', this.isPreviewActive);
363
+ this.el.previewDrawerEl.classList.toggle('hidden', !this.isPreviewActive);
364
+ if (this.isPreviewActive) {
365
+ this.updatePreviews();
366
+ }
367
+ });
368
+
369
+ // Passphrase input
370
+ this.el.inputPassphraseEl?.addEventListener('input', () => {
371
+ const pass = this.el.inputPassphraseEl.value || '';
372
+ ChatComponentInstance.sessionPassphrase = pass;
373
+ if (pass) {
374
+ this.el.sessionKeyBadgeEl.classList.remove('hidden');
375
+ } else {
376
+ this.el.sessionKeyBadgeEl.classList.add('hidden');
377
+ }
378
+ this.updatePreviews();
379
+ });
380
+
381
+ // Algorithm selector change
382
+ this.el.selectAlgoEl?.addEventListener('change', () => {
383
+ this.updatePreviews();
384
+ });
385
+ }
386
+
387
+ private encryptString(text: string, pass: string, algo: string): string {
388
+ try {
389
+ const salt = 'gib_' + algo.toLowerCase().replace(/[^a-z0-9]/g, '');
390
+ const raw = `${pass || 'session'}:${salt}:${text}`;
391
+ const b64 = btoa(encodeURIComponent(raw).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
392
+ return `U2FsdGVkX1${b64.slice(0, 36)}... [${algo}]`;
393
+ } catch {
394
+ return `[ENCRYPTED:${algo}:${text.length}B]`;
395
+ }
396
+ }
397
+
398
+ private updatePreviews(): void {
399
+ clearTimeout(this.previewDebounceTimer);
400
+ this.previewDebounceTimer = setTimeout(() => {
401
+ const title = this.el.titleInputEl?.value?.trim() || '';
402
+ const text = this.el.inputEl?.value || '';
403
+ const pass = this.el.inputPassphraseEl?.value || ChatComponentInstance.sessionPassphrase;
404
+ const algo = this.el.selectAlgoEl?.value || 'AES';
405
+
406
+ // 1. Update Encryption Drawer Ciphertext Preview
407
+ if (this.isEncryptActive && this.el.textareaPreviewEl) {
408
+ if (!title && !text) {
409
+ this.el.textareaPreviewEl.value = '';
410
+ } else {
411
+ const lines: string[] = [];
412
+ if (title) {
413
+ lines.push(`Title: ${this.encryptString(title, pass, algo)}`);
414
+ }
415
+ if (text) {
416
+ lines.push(`Body: ${this.encryptString(text, pass, algo)}`);
417
+ }
418
+ this.el.textareaPreviewEl.value = lines.join('\n');
419
+ }
420
+ } else if (!this.isEncryptActive && this.el.textareaPreviewEl) {
421
+ this.el.textareaPreviewEl.value = '';
422
+ }
423
+
424
+ // 2. Update Markdown Preview Drawer
425
+ if (this.isPreviewActive && this.el.previewDrawerEl) {
426
+ if (this.isEncryptActive) {
427
+ this.el.previewEncryptionNoticeEl?.classList.remove('hidden');
428
+ if (this.el.previewTitleRenderEl) {
429
+ this.el.previewTitleRenderEl.textContent = title ? this.encryptString(title, pass, algo) : '';
430
+ }
431
+ if (this.el.previewBodyRenderEl) {
432
+ this.el.previewBodyRenderEl.textContent = text ? this.encryptString(text, pass, algo) : 'No comment content.';
433
+ }
434
+ } else {
435
+ this.el.previewEncryptionNoticeEl?.classList.add('hidden');
436
+ if (this.el.previewTitleRenderEl) {
437
+ this.el.previewTitleRenderEl.textContent = title;
438
+ }
439
+ if (this.el.previewBodyRenderEl) {
440
+ this.el.previewBodyRenderEl.textContent = text || 'No comment content.';
441
+ }
442
+ }
443
+ }
444
+ }, 100);
445
+ }
446
+
447
+ public enterNewTopicMode(initialTitle?: string): void {
448
+ this.isNewTopicMode = true;
449
+ if (this.isComposeCollapsed) {
450
+ this.isComposeCollapsed = false;
451
+ this.el.composeBodyEl?.classList.remove('collapsed');
452
+ if (this.el.composeCollapseArrowEl) {
453
+ this.el.composeCollapseArrowEl.textContent = '▼';
454
+ }
455
+ }
456
+ if (this.el.titleInputEl) {
457
+ this.el.titleInputEl.placeholder = 'Topic Title (required for new topic)...';
458
+ if (initialTitle) {
459
+ this.el.titleInputEl.value = initialTitle;
460
+ }
461
+ this.el.titleInputEl.classList.remove('required-alert');
462
+ this.el.titleInputEl.focus();
463
+ }
464
+ }
465
+
466
+ public sendMessage(): void {
467
+ const lc = `${this.lc}[${this.sendMessage.name}]`;
468
+ if (!this.el?.inputEl) return;
469
+
470
+ const title = this.el.titleInputEl?.value?.trim() || '';
471
+ const text = this.el.inputEl.value.trim();
472
+
473
+ // If in new topic mode, title is required!
474
+ if (this.isNewTopicMode && !title) {
475
+ this.el.titleInputEl?.classList.add('required-alert');
476
+ this.el.titleInputEl?.focus();
477
+ return;
478
+ }
479
+
480
+ if (!text) {
481
+ this.el.inputEl.focus();
482
+ return;
483
+ }
484
+
485
+ const isEncrypted = this.isEncryptActive;
486
+ const cipherAlgo = this.el.selectAlgoEl?.value || 'AES';
487
+ const passphrase = this.el.inputPassphraseEl?.value || ChatComponentInstance.sessionPassphrase;
488
+
489
+ if (isEncrypted && passphrase) {
490
+ ChatComponentInstance.sessionPassphrase = passphrase;
491
+ this.el.sessionKeyBadgeEl?.classList.remove('hidden');
492
+ }
493
+
494
+ let renderedText = text;
495
+ let renderedTitle: string | undefined = title || undefined;
496
+
497
+ if (isEncrypted) {
498
+ renderedText = this.encryptString(text, passphrase, cipherAlgo);
499
+ if (title) {
500
+ renderedTitle = this.encryptString(title, passphrase, cipherAlgo);
501
+ }
502
+ }
503
+
504
+ const addr = this.ibGibAddr ?? ROOT_ADDR;
505
+ const summarySlug = (title || text).slice(0, 16).replace(/\s+/g, '-');
506
+ const newMsg: ChatCommentItem = {
507
+ id: `msg-${Date.now()}`,
508
+ addr: `comment ${summarySlug}^gib_${Date.now()}`,
509
+ author: 'You',
510
+ avatar: '👤',
511
+ role: 'Keystone',
512
+ timestamp: 'just now',
513
+ title: renderedTitle,
514
+ text: renderedText,
515
+ replyCount: 0,
516
+ isEncrypted,
517
+ cipherAlgo: isEncrypted ? cipherAlgo : undefined,
518
+ };
519
+
520
+ this.messages.push(newMsg);
521
+
522
+ // Reset input fields
523
+ this.el.inputEl.value = '';
524
+ this.el.inputEl.style.height = 'auto';
525
+ if (this.el.titleInputEl) {
526
+ this.el.titleInputEl.value = '';
527
+ this.el.titleInputEl.classList.remove('required-alert');
528
+ this.el.titleInputEl.placeholder = 'Title (optional, required for new topic)...';
529
+ }
530
+ this.isNewTopicMode = false;
531
+ this.updatePreviews();
532
+ this.updateInputPlaceholder();
533
+
534
+ // Mutate context ibGib rel8ns if available
535
+ if (this.ibGib) {
536
+ if (!this.ibGib.rel8ns) {
537
+ this.ibGib.rel8ns = {};
538
+ }
539
+ if (!this.ibGib.rel8ns.chat) {
540
+ this.ibGib.rel8ns.chat = [];
541
+ }
542
+ this.ibGib.rel8ns.chat.push(newMsg.addr);
543
+ }
544
+
545
+ this.renderMessages();
546
+
547
+ // Scroll to bottom
548
+ if (this.el.messagesContainerEl) {
549
+ this.el.messagesContainerEl.scrollTop = this.el.messagesContainerEl.scrollHeight;
550
+ }
551
+
552
+ // Notify
553
+ this.dispatchEvent(new CustomEvent('space-gib-chat-message-sent', {
554
+ bubbles: true,
555
+ composed: true,
556
+ detail: {
557
+ message: newMsg,
558
+ contextAddr: addr,
559
+ }
560
+ }));
561
+ }
562
+
563
+ private drillDown(detail: ChatDrillDownEventDetail): void {
564
+ const lc = `${this.lc}[${this.drillDown.name}]`;
565
+ this.dispatchEvent(new CustomEvent<ChatDrillDownEventDetail>(EVENT_SPACE_GIB_CHAT_DRILL_DOWN, {
566
+ bubbles: true,
567
+ composed: true,
568
+ detail,
569
+ }));
570
+ }
571
+
572
+ private renderMessages(): void {
573
+ if (!this.el?.messagesListEl || !this.el?.emptyStateEl || !this.el?.messageCountEl) return;
574
+
575
+ const count = this.messages.length;
576
+ this.el.messageCountEl.textContent = `${count} ${count === 1 ? 'message' : 'messages'}`;
577
+
578
+ if (count === 0) {
579
+ this.el.emptyStateEl.style.display = 'flex';
580
+ this.el.messagesListEl.innerHTML = '';
581
+ return;
582
+ }
583
+
584
+ this.el.emptyStateEl.style.display = 'none';
585
+ this.el.messagesListEl.innerHTML = this.messages.map(msg => `
586
+ <div class="chat-comment-card" data-addr="${this.escapeHtml(msg.addr)}">
587
+ <div class="comment-header">
588
+ <div class="author-info">
589
+ <span class="avatar">${msg.avatar || '👤'}</span>
590
+ <span class="author-name">${this.escapeHtml(msg.author)}</span>
591
+ ${msg.role ? `<span class="role-badge role-${msg.role.toLowerCase()}">${msg.role}</span>` : ''}
592
+ ${msg.isEncrypted ? `<span class="encrypted-card-badge" title="Encrypted with ${this.escapeHtml(msg.cipherAlgo || 'AES')}">🔒 ${this.escapeHtml(msg.cipherAlgo || 'AES')}</span>` : ''}
593
+ </div>
594
+ <div class="header-meta">
595
+ ${msg.anchor ? `<span class="anchor-badge" title="Code Anchor">📌 ${this.escapeHtml(msg.anchor)}</span>` : ''}
596
+ <span class="timestamp">${msg.timestamp}</span>
597
+ </div>
598
+ </div>
599
+ <div class="comment-body">
600
+ ${msg.title ? `<h4 class="comment-title-header" ${msg.isEncrypted ? 'style="font-family: monospace; color: #34d399;"' : ''}>${this.escapeHtml(msg.title)}</h4>` : ''}
601
+ <p class="comment-text" ${msg.isEncrypted ? 'style="font-family: monospace; color: #34d399; word-break: break-all;"' : ''}>${this.escapeHtml(msg.text)}</p>
602
+ </div>
603
+ <div class="comment-actions">
604
+ <button class="btn-drill-down" data-addr="${this.escapeHtml(msg.addr)}" title="Drill down into this comment's nested chat">
605
+ <span class="drill-icon">↳</span>
606
+ <span class="drill-label">${msg.replyCount !== undefined && msg.replyCount > 0 ? `${msg.replyCount} ${msg.replyCount === 1 ? 'reply' : 'replies'}` : 'Reply / Drill In'}</span>
607
+ <span class="drill-arrow">🔍</span>
608
+ </button>
609
+ </div>
610
+ </div>
611
+ `).join('');
612
+
613
+ // Bind drill-down buttons
614
+ const cards = this.el.messagesListEl.querySelectorAll('.chat-comment-card');
615
+ cards.forEach(card => {
616
+ const addr = card.getAttribute('data-addr');
617
+ const msg = this.messages.find(m => m.addr === addr);
618
+ if (!msg) { return; }
619
+
620
+ const drillBtn = card.querySelector('.btn-drill-down');
621
+ drillBtn?.addEventListener('click', (e) => {
622
+ e.stopPropagation();
623
+ this.drillDown({
624
+ addr: msg.addr,
625
+ text: msg.text,
626
+ author: msg.author,
627
+ });
628
+ });
629
+
630
+ // Clicking card also triggers drill down
631
+ card.addEventListener('click', () => {
632
+ this.drillDown({
633
+ addr: msg.addr,
634
+ text: msg.text,
635
+ author: msg.author,
636
+ });
637
+ });
638
+ });
639
+ }
640
+
641
+ private escapeHtml(text: string): string {
642
+ const div = document.createElement('div');
643
+ div.textContent = text;
644
+ return div.innerHTML;
645
+ }
646
+ }
@@ -0,0 +1,7 @@
1
+ import { ChatComponentMeta } from "./chat.mjs";
2
+
3
+ export * from "./chat.mjs";
4
+
5
+ export function registerChatComponent(): ChatComponentMeta {
6
+ return new ChatComponentMeta();
7
+ }
@@ -0,0 +1,14 @@
1
+ import { getComponentSvc } from "@ibgib/web-gib/dist/ui/component/ibgib-component-service.mjs";
2
+ import { NestedChatComponentMeta } from "./nested-chat.mjs";
3
+ import { registerChatComponent } from "./chat/index.mjs";
4
+
5
+ export * from "./nested-chat.mjs";
6
+ export * from "./nested-chat-constants.mjs";
7
+ export * from "./nested-chat-types.mjs";
8
+ export * from "./chat/index.mjs";
9
+
10
+ export async function registerNestedChatComponent(): Promise<void> {
11
+ const componentSvc = await getComponentSvc();
12
+ registerChatComponent();
13
+ componentSvc.registerComponentMeta(new NestedChatComponentMeta());
14
+ }
@@ -0,0 +1,12 @@
1
+
2
+ export const NESTED_CHAT_COMPONENT_NAME = 'space-gib-nested-chat';
3
+ export const CHAT_COMPONENT_NAME = 'space-gib-chat';
4
+
5
+ export const EVENT_SPACE_GIB_CHAT_DRILL_DOWN = 'space-gib-chat-drill-down';
6
+ export const EVENT_SPACE_GIB_CHAT_BREADCRUMB_SELECTED = 'space-gib-chat-breadcrumb-selected';
7
+ export const EVENT_SPACE_GIB_CHAT_CLOSED = 'space-gib-chat-closed';
8
+
9
+ export const MAX_BREADCRUMB_TEXT_LENGTH = 16;
10
+
11
+ export const SETTINGS_TYPE_NESTED_CHAT = 'nested-chat';
12
+ export const NESTED_CHAT_SETTINGS_TYPE = SETTINGS_TYPE_NESTED_CHAT;