collabmd 0.1.21 → 0.1.23

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 (99) hide show
  1. package/README.md +94 -7
  2. package/bin/collabmd.js +3 -2
  3. package/docker-compose.yml +13 -1
  4. package/package.json +2 -1
  5. package/public/assets/css/style.css +1 -1
  6. package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
  7. package/public/assets/js/chunks/editor-session-S5AT7BMV.js +22 -0
  8. package/public/assets/js/{excalidraw-editor-TCIH6GJL.js → excalidraw-editor-5YWMGLKF.js} +145 -62
  9. package/public/assets/js/excalidraw-editor.js +1 -1
  10. package/public/assets/js/main.js +503 -182
  11. package/public/index.html +104 -84
  12. package/src/client/application/app-shell/git-feature.js +247 -9
  13. package/src/client/application/app-shell/ui-feature.js +65 -7
  14. package/src/client/application/app-shell/workspace-feature.js +46 -0
  15. package/src/client/application/app-shell-elements.js +2 -0
  16. package/src/client/application/mermaid-preview-hydrator.js +1 -1
  17. package/src/client/application/plantuml-preview-hydrator.js +1 -1
  18. package/src/client/application/workspace-coordinator.js +147 -18
  19. package/src/client/application/workspace-preview-controller.js +34 -0
  20. package/src/client/application/workspace-route-controller.js +104 -0
  21. package/src/client/bootstrap/collabmd-app-shell.js +95 -0
  22. package/src/client/domain/runtime-paths.js +5 -0
  23. package/src/client/excalidraw-editor.js +50 -2
  24. package/src/client/infrastructure/auth-client.js +219 -52
  25. package/src/client/infrastructure/browser-navigation-port.js +20 -0
  26. package/src/client/infrastructure/editor-collaboration-client.js +4 -0
  27. package/src/client/infrastructure/editor-session.js +47 -7
  28. package/src/client/infrastructure/editor-view-adapter.js +122 -58
  29. package/src/client/infrastructure/runtime-config.js +66 -0
  30. package/src/client/presentation/comment-ui-controller.js +82 -26
  31. package/src/client/presentation/comments-panel.js +2 -2
  32. package/src/client/presentation/excalidraw-embed-controller.js +60 -2
  33. package/src/client/presentation/file-history-view-controller.js +806 -0
  34. package/src/client/presentation/git-diff-view-controller.js +718 -121
  35. package/src/client/presentation/git-panel-controller.js +458 -66
  36. package/src/client/presentation/layout-controller.js +1 -1
  37. package/src/client/styles/components/actions.css +89 -0
  38. package/src/client/styles/components/feedback.css +43 -0
  39. package/src/client/styles/components/navigation.css +68 -0
  40. package/src/client/styles/components/scrollbars.css +104 -0
  41. package/src/client/styles/components/states.css +289 -0
  42. package/src/client/styles/components/surfaces.css +155 -0
  43. package/src/client/styles/features/backlinks.css +213 -0
  44. package/src/client/styles/features/collaboration-chat.css +228 -0
  45. package/src/client/styles/features/collaboration-dialogs.css +154 -0
  46. package/src/client/styles/features/collaboration-presence.css +103 -0
  47. package/src/client/styles/features/collaboration-ui.css +3 -0
  48. package/src/client/styles/features/comment-card.css +236 -0
  49. package/src/client/styles/features/comment-markdown.css +127 -0
  50. package/src/client/styles/features/comment-overlays.css +60 -0
  51. package/src/client/styles/features/comments-drawer.css +120 -0
  52. package/src/client/styles/features/comments.css +4 -0
  53. package/src/client/styles/features/diagram-preview.css +227 -0
  54. package/src/client/styles/features/diff-commit.css +268 -0
  55. package/src/client/styles/features/diff-lines.css +160 -0
  56. package/src/client/styles/features/diff-toolbar.css +98 -0
  57. package/src/client/styles/features/diff.css +3 -0
  58. package/src/client/styles/features/embedded-preview.css +218 -0
  59. package/src/client/styles/features/git-branch.css +155 -0
  60. package/src/client/styles/features/git-files.css +105 -0
  61. package/src/client/styles/features/git-history.css +339 -0
  62. package/src/client/styles/features/git.css +3 -0
  63. package/src/client/styles/features/media-lightbox.css +157 -0
  64. package/src/client/styles/features/outline.css +115 -0
  65. package/src/client/styles/features/preview-content.css +3 -0
  66. package/src/client/styles/features/preview-markdown.css +201 -0
  67. package/src/client/styles/features/preview-sidebars.css +2 -0
  68. package/src/client/styles/features/quick-switcher.css +167 -0
  69. package/src/client/styles/foundation/themes.css +347 -0
  70. package/src/client/styles/foundation/tokens.css +66 -0
  71. package/src/client/styles/foundation/utilities.css +36 -0
  72. package/src/client/styles/layout/editor-page.css +203 -0
  73. package/src/client/styles/layout/overlays.css +132 -0
  74. package/src/client/styles/layout/responsive.css +251 -0
  75. package/src/client/styles/layout/shell.css +314 -0
  76. package/src/client/styles/layout/sidebar.css +176 -0
  77. package/src/client/styles/layout/view-modes.css +92 -0
  78. package/src/client/styles/overrides/codemirror.css +53 -0
  79. package/src/client/styles/overrides/highlightjs.css +7 -0
  80. package/src/client/styles/overrides/vendor.css +2 -0
  81. package/src/client/styles/primitives/controls.css +276 -0
  82. package/src/client/styles/style.css +25 -4945
  83. package/src/server/auth/create-auth-service.js +373 -15
  84. package/src/server/auth/session-cookie.js +30 -4
  85. package/src/server/config/env.js +121 -5
  86. package/src/server/create-app-server.js +8 -0
  87. package/src/server/domain/collaboration/collaboration-room.js +90 -47
  88. package/src/server/index.js +3 -1
  89. package/src/server/infrastructure/git/command-runner.js +2 -1
  90. package/src/server/infrastructure/git/git-service.js +48 -2
  91. package/src/server/infrastructure/git/history-service.js +683 -0
  92. package/src/server/infrastructure/git/parsers.js +30 -0
  93. package/src/server/infrastructure/git/responses.js +73 -0
  94. package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
  95. package/src/server/infrastructure/http/create-git-api-command-handler.js +2 -0
  96. package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
  97. package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
  98. package/src/server/infrastructure/http/create-request-handler.js +15 -0
  99. package/public/assets/js/chunks/editor-session-EAUBJCHH.js +0 -22
@@ -340,8 +340,8 @@ export class EditorViewAdapter {
340
340
  };
341
341
  }
342
342
 
343
- initialize({ awareness, filePath, undoManager, ytext }) {
344
- const updateListener = EditorView.updateListener.of((update) => {
343
+ createUpdateListener() {
344
+ return EditorView.updateListener.of((update) => {
345
345
  if (update.docChanged) {
346
346
  this.onDocChanged?.();
347
347
  }
@@ -351,62 +351,99 @@ export class EditorViewAdapter {
351
351
  this.onSelectionChanged?.(update.state);
352
352
  }
353
353
  });
354
+ }
354
355
 
355
- const loadingIndicator = this.editorContainer.querySelector('#editorLoading');
356
+ getBaseExtensions(filePath, { readOnly = false } = {}) {
357
+ const extensions = [
358
+ lineNumbers(),
359
+ highlightActiveLineGutter(),
360
+ highlightSpecialChars(),
361
+ history(),
362
+ foldGutter(),
363
+ drawSelection(),
364
+ EditorState.allowMultipleSelections.of(true),
365
+ indentOnInput(),
366
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
367
+ bracketMatching({ renderMatch: renderBracketMatch }),
368
+ closeBrackets(),
369
+ autocompletion({
370
+ override: [wikiLinkCompletions(this.getFileList)],
371
+ }),
372
+ rectangularSelection(),
373
+ crosshairCursor(),
374
+ highlightActiveLine(),
375
+ highlightSelectionMatches(),
376
+ keymap.of([
377
+ ...closeBracketsKeymap,
378
+ ...defaultKeymap,
379
+ ...historyKeymap,
380
+ ...searchKeymap,
381
+ ...foldKeymap,
382
+ indentWithTab,
383
+ ]),
384
+ EditorView.contentAttributes.of({
385
+ 'aria-label': 'Markdown editor',
386
+ }),
387
+ createLanguageExtension(filePath),
388
+ this.themeCompartment.of(createEditorTheme(this.initialTheme)),
389
+ this.syntaxThemeCompartment.of(this.initialTheme === 'dark' ? oneDark : []),
390
+ this.lineWrappingCompartment.of(this.lineWrappingEnabled ? EditorView.lineWrapping : []),
391
+ remoteUpdateFlashField,
392
+ this.createUpdateListener(),
393
+ ];
394
+
395
+ if (readOnly) {
396
+ extensions.push(EditorState.readOnly.of(true));
397
+ extensions.push(EditorView.editable.of(false));
398
+ return extensions;
399
+ }
400
+
401
+ extensions.splice(13, 0, Prec.highest(EditorView.domEventHandlers({
402
+ paste: (event) => handleImagePasteEvent(event, this.onImagePaste),
403
+ })));
404
+ return extensions;
405
+ }
406
+
407
+ clearEditorDom({ preserveLoadingIndicator = true } = {}) {
408
+ if (!this.editorContainer) {
409
+ return;
410
+ }
411
+
412
+ const loadingIndicator = preserveLoadingIndicator
413
+ ? this.editorContainer.querySelector('#editorLoading')
414
+ : null;
356
415
  Array.from(this.editorContainer.children).forEach((child) => {
357
416
  if (child !== loadingIndicator) {
358
417
  child.remove();
359
418
  }
360
419
  });
420
+ }
421
+
422
+ teardownEditorView({ clearContainer = false } = {}) {
423
+ this.editorView?.contentDOM?.removeEventListener('beforeinput', this.handleLocalInputActivity);
424
+ this.editorView?.contentDOM?.removeEventListener('keydown', this.handleLocalInputActivity);
425
+ this.editorView?.contentDOM?.removeEventListener('paste', this.handleLocalInputActivity);
426
+ this.editorView?.contentDOM?.removeEventListener('compositionstart', this.handleLocalInputActivity);
427
+ this.editorView?.scrollDOM?.removeEventListener('scroll', this.handleScroll);
428
+ this.editorView?.destroy();
429
+ this.editorView = null;
430
+
431
+ if (clearContainer && this.editorContainer) {
432
+ this.editorContainer.innerHTML = '';
433
+ delete this.editorContainer.dataset.editorMode;
434
+ }
435
+ }
436
+
437
+ mountEditor(state, { editorMode = 'collaborative', preserveScrollTop = 0 } = {}) {
438
+ this.clearEditorDom({ preserveLoadingIndicator: true });
439
+ this.teardownEditorView();
361
440
 
362
441
  this.editorView = new EditorView({
363
442
  parent: this.editorContainer,
364
- state: EditorState.create({
365
- doc: ytext.toString(),
366
- extensions: [
367
- lineNumbers(),
368
- highlightActiveLineGutter(),
369
- highlightSpecialChars(),
370
- history(),
371
- foldGutter(),
372
- drawSelection(),
373
- EditorState.allowMultipleSelections.of(true),
374
- indentOnInput(),
375
- syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
376
- bracketMatching({ renderMatch: renderBracketMatch }),
377
- closeBrackets(),
378
- autocompletion({
379
- override: [wikiLinkCompletions(this.getFileList)],
380
- }),
381
- rectangularSelection(),
382
- crosshairCursor(),
383
- highlightActiveLine(),
384
- highlightSelectionMatches(),
385
- keymap.of([
386
- ...closeBracketsKeymap,
387
- ...defaultKeymap,
388
- ...historyKeymap,
389
- ...searchKeymap,
390
- ...foldKeymap,
391
- indentWithTab,
392
- ]),
393
- EditorView.contentAttributes.of({
394
- 'aria-label': 'Markdown editor',
395
- }),
396
- Prec.highest(EditorView.domEventHandlers({
397
- paste: (event) => handleImagePasteEvent(event, this.onImagePaste),
398
- })),
399
- createLanguageExtension(filePath),
400
- this.themeCompartment.of(createEditorTheme(this.initialTheme)),
401
- this.syntaxThemeCompartment.of(this.initialTheme === 'dark' ? oneDark : []),
402
- this.lineWrappingCompartment.of(this.lineWrappingEnabled ? EditorView.lineWrapping : []),
403
- remoteUpdateFlashField,
404
- yCollab(ytext, awareness, { undoManager }),
405
- updateListener,
406
- ],
407
- }),
443
+ state,
408
444
  });
409
445
 
446
+ this.editorContainer.dataset.editorMode = editorMode;
410
447
  this.editorView.scrollDOM.addEventListener('scroll', this.handleScroll, { passive: true });
411
448
  this.editorView.contentDOM.addEventListener('beforeinput', this.handleLocalInputActivity);
412
449
  this.editorView.contentDOM.addEventListener('keydown', this.handleLocalInputActivity);
@@ -415,6 +452,43 @@ export class EditorViewAdapter {
415
452
  this.updateCursorInfo(this.editorView.state);
416
453
  this.onSelectionChanged?.(this.editorView.state);
417
454
  this.emitViewportChange();
455
+
456
+ if (preserveScrollTop > 0) {
457
+ requestAnimationFrame(() => {
458
+ if (this.editorView?.scrollDOM) {
459
+ this.editorView.scrollDOM.scrollTop = preserveScrollTop;
460
+ }
461
+ });
462
+ }
463
+ }
464
+
465
+ initialize({ awareness, filePath, undoManager, ytext }) {
466
+ const preserveScrollTop = this.editorView?.scrollDOM?.scrollTop ?? 0;
467
+ const state = EditorState.create({
468
+ doc: ytext.toString(),
469
+ extensions: [
470
+ ...this.getBaseExtensions(filePath),
471
+ yCollab(ytext, awareness, { undoManager }),
472
+ ],
473
+ });
474
+
475
+ this.mountEditor(state, {
476
+ editorMode: 'collaborative',
477
+ preserveScrollTop,
478
+ });
479
+ }
480
+
481
+ initializeProvisional({ content = '', filePath }) {
482
+ const preserveScrollTop = this.editorView?.scrollDOM?.scrollTop ?? 0;
483
+ const state = EditorState.create({
484
+ doc: String(content ?? ''),
485
+ extensions: this.getBaseExtensions(filePath, { readOnly: true }),
486
+ });
487
+
488
+ this.mountEditor(state, {
489
+ editorMode: 'provisional',
490
+ preserveScrollTop,
491
+ });
418
492
  }
419
493
 
420
494
  destroy() {
@@ -426,17 +500,7 @@ export class EditorViewAdapter {
426
500
  clearTimeout(this.remoteUpdateFlashTimer);
427
501
  this.remoteUpdateFlashTimer = 0;
428
502
  }
429
- this.editorView?.contentDOM?.removeEventListener('beforeinput', this.handleLocalInputActivity);
430
- this.editorView?.contentDOM?.removeEventListener('keydown', this.handleLocalInputActivity);
431
- this.editorView?.contentDOM?.removeEventListener('paste', this.handleLocalInputActivity);
432
- this.editorView?.contentDOM?.removeEventListener('compositionstart', this.handleLocalInputActivity);
433
- this.editorView?.scrollDOM?.removeEventListener('scroll', this.handleScroll);
434
- this.editorView?.destroy();
435
- this.editorView = null;
436
-
437
- if (this.editorContainer) {
438
- this.editorContainer.innerHTML = '';
439
- }
503
+ this.teardownEditorView({ clearContainer: true });
440
504
  }
441
505
 
442
506
  getText() {
@@ -30,6 +30,36 @@ function normalizeDiffScope(scope) {
30
30
  export function getHashRoute() {
31
31
  const params = getHashParams();
32
32
 
33
+ if (params.has('git-file-preview')) {
34
+ const historicalFilePath = params.get('git-file-preview') || null;
35
+ return {
36
+ currentFilePath: params.get('current') || historicalFilePath,
37
+ filePath: historicalFilePath,
38
+ hash: params.get('hash') || null,
39
+ type: 'git-file-preview',
40
+ };
41
+ }
42
+
43
+ if (params.has('git-file-history')) {
44
+ return {
45
+ filePath: params.get('git-file-history') || null,
46
+ type: 'git-file-history',
47
+ };
48
+ }
49
+
50
+ if (params.has('git-history')) {
51
+ return { type: 'git-history' };
52
+ }
53
+
54
+ if (params.has('git-commit')) {
55
+ return {
56
+ hash: params.get('git-commit') || null,
57
+ historyFilePath: params.get('history') || null,
58
+ path: params.get('path') || null,
59
+ type: 'git-commit',
60
+ };
61
+ }
62
+
33
63
  if (params.has('git-diff')) {
34
64
  const filePath = params.get('git-diff') || null;
35
65
  return {
@@ -63,3 +93,39 @@ export function navigateToGitDiff({ filePath = null, scope = 'all' } = {}) {
63
93
  params.set('scope', normalizeDiffScope(scope));
64
94
  window.location.hash = params.toString();
65
95
  }
96
+
97
+ export function navigateToGitCommit({ hash, path = null, historyFilePath = null } = {}) {
98
+ const normalizedHash = String(hash ?? '').trim();
99
+ const params = new URLSearchParams();
100
+ params.set('git-commit', normalizedHash);
101
+ if (historyFilePath) {
102
+ params.set('history', historyFilePath);
103
+ }
104
+ if (path) {
105
+ params.set('path', path);
106
+ }
107
+ window.location.hash = params.toString();
108
+ }
109
+
110
+ export function navigateToGitHistory() {
111
+ const params = new URLSearchParams();
112
+ params.set('git-history', '1');
113
+ window.location.hash = params.toString();
114
+ }
115
+
116
+ export function navigateToGitFileHistory({ filePath } = {}) {
117
+ const params = new URLSearchParams();
118
+ params.set('git-file-history', filePath ?? '');
119
+ window.location.hash = params.toString();
120
+ }
121
+
122
+ export function navigateToGitFilePreview({ hash, path, currentFilePath = null } = {}) {
123
+ const normalizedHash = String(hash ?? '').trim();
124
+ const params = new URLSearchParams();
125
+ params.set('git-file-preview', path ?? '');
126
+ if (currentFilePath) {
127
+ params.set('current', currentFilePath);
128
+ }
129
+ params.set('hash', normalizedHash);
130
+ window.location.hash = params.toString();
131
+ }
@@ -9,6 +9,7 @@ const COMMENT_CARD_WIDTH = 520;
9
9
  const COMMENT_SELECTION_REVEAL_DELAY_MS = 150;
10
10
  const COMMENT_SELECTION_CHIP_GAP = 12;
11
11
  const COMMENT_CONTROL_SLOT_HEIGHT = 36;
12
+ const COMMENT_PREVIEW_BADGE_MIN_WIDTH = 28;
12
13
  const COMMENT_PREVIEW_RAIL_SLOT_HEIGHT = 30;
13
14
  const COMMENT_PREVIEW_RAIL_MIN_WIDTH = 400;
14
15
  const COMMENT_PREVIEW_RAIL_BREAKPOINT = 769;
@@ -670,6 +671,51 @@ export class CommentUiController {
670
671
  this.repositionActiveCard();
671
672
  }
672
673
 
674
+ syncPreviewRailLayout(maxBubbleWidth = 0) {
675
+ if (!this.previewElement) {
676
+ return false;
677
+ }
678
+
679
+ const shouldShowRail = this.supported && maxBubbleWidth > 0;
680
+ const nextReserved = 0;
681
+ const nextOffset = 0;
682
+ let reserved = nextReserved;
683
+ let offset = nextOffset;
684
+
685
+ if (shouldShowRail) {
686
+ const previewStyle = getComputedStyle(this.previewElement);
687
+ const currentReserved = Number.parseFloat(
688
+ previewStyle.getPropertyValue('--preview-comment-rail-reserved'),
689
+ ) || 0;
690
+ const currentPaddingRight = Number.parseFloat(previewStyle.paddingRight) || 0;
691
+ const railInset = Number.parseFloat(
692
+ previewStyle.getPropertyValue('--preview-comment-rail-inset'),
693
+ ) || 0;
694
+ const basePaddingRight = Math.max(currentPaddingRight - currentReserved, 0);
695
+ const requiredRail = Math.max(maxBubbleWidth + railInset - basePaddingRight, 0);
696
+ const previewContainerRect = this.previewContainer?.getBoundingClientRect();
697
+ const previewRect = this.previewElement.getBoundingClientRect();
698
+ const availableRightGutter = previewContainerRect
699
+ ? Math.max(previewContainerRect.right - previewRect.right, 0)
700
+ : 0;
701
+
702
+ offset = Math.min(availableRightGutter, requiredRail);
703
+ reserved = Math.max(requiredRail - offset, 0);
704
+ }
705
+
706
+ const nextReservedValue = `${Math.ceil(reserved)}px`;
707
+ const nextOffsetValue = `${Math.floor(offset)}px`;
708
+ const didChange = this.previewElement.style.getPropertyValue('--preview-comment-rail-reserved') !== nextReservedValue
709
+ || this.previewElement.style.getPropertyValue('--preview-comment-rail-offset') !== nextOffsetValue;
710
+
711
+ if (didChange) {
712
+ this.previewElement.style.setProperty('--preview-comment-rail-reserved', nextReservedValue);
713
+ this.previewElement.style.setProperty('--preview-comment-rail-offset', nextOffsetValue);
714
+ }
715
+
716
+ return didChange;
717
+ }
718
+
673
719
  scheduleLayoutRefresh() {
674
720
  if (this.layoutFrame) {
675
721
  return;
@@ -700,7 +746,7 @@ export class CommentUiController {
700
746
  this.commentsToggleButton.classList.toggle('active', this.drawerOpen);
701
747
  this.commentsToggleButton.setAttribute('aria-expanded', String(this.drawerOpen));
702
748
  const label = totalCount > 0 ? `Comments ${totalCount}` : 'Comments';
703
- const labelElement = this.commentsToggleButton.querySelector('.pane-header-btn-label');
749
+ const labelElement = this.commentsToggleButton.querySelector('.ui-action-label');
704
750
  if (labelElement) {
705
751
  labelElement.textContent = label;
706
752
  } else {
@@ -726,7 +772,7 @@ export class CommentUiController {
726
772
  groups.forEach((group) => {
727
773
  const button = document.createElement('button');
728
774
  button.type = 'button';
729
- button.className = 'comments-drawer-item';
775
+ button.className = 'ui-record-surface comments-drawer-item';
730
776
  button.classList.toggle('is-active', this.activeCard?.groupKey === group.key);
731
777
  button.addEventListener('pointerdown', (event) => {
732
778
  event.preventDefault();
@@ -740,14 +786,14 @@ export class CommentUiController {
740
786
  });
741
787
 
742
788
  const header = document.createElement('div');
743
- header.className = 'comments-drawer-item-header';
789
+ header.className = 'ui-record-header comments-drawer-item-header';
744
790
 
745
791
  const title = document.createElement('span');
746
792
  title.className = 'comments-drawer-item-title';
747
793
  title.textContent = formatAnchorLabel(group.anchor);
748
794
 
749
795
  const count = document.createElement('span');
750
- count.className = 'comments-drawer-item-count';
796
+ count.className = 'ui-pill-badge ui-pill-badge--count ui-pill-badge--accent comments-drawer-item-count';
751
797
  count.textContent = String(group.threads.length);
752
798
 
753
799
  header.append(title, count);
@@ -763,7 +809,7 @@ export class CommentUiController {
763
809
  );
764
810
 
765
811
  const footer = document.createElement('div');
766
- footer.className = 'comments-drawer-item-footer';
812
+ footer.className = 'ui-record-meta comments-drawer-item-footer';
767
813
  const countLabel = document.createElement('span');
768
814
  countLabel.textContent = `${group.threads.length} thread${group.threads.length === 1 ? '' : 's'}`;
769
815
 
@@ -822,7 +868,7 @@ export class CommentUiController {
822
868
 
823
869
  const button = document.createElement('button');
824
870
  button.type = 'button';
825
- button.className = 'comment-editor-badge';
871
+ button.className = 'ui-state-marker ui-state-marker--comment comment-editor-badge';
826
872
  button.dataset.count = String(group.threads.length);
827
873
  const isActive = this.activeCard?.groupKey === group.key;
828
874
  const isHovered = this.hoveredEditorGroupKeys.includes(group.key);
@@ -886,7 +932,7 @@ export class CommentUiController {
886
932
  }
887
933
  const button = document.createElement('button');
888
934
  button.type = 'button';
889
- button.className = 'comment-selection-chip';
935
+ button.className = 'ui-selection-pill ui-selection-pill--comment comment-selection-chip';
890
936
  button.textContent = 'Comment';
891
937
  button.style.top = `${chipTop}px`;
892
938
  button.style.right = `${COMMENT_SELECTION_CHIP_GAP}px`;
@@ -920,6 +966,7 @@ export class CommentUiController {
920
966
 
921
967
  if (!this.supported || !this.previewElement) {
922
968
  this.previewHoverRegions = [];
969
+ this.syncPreviewRailLayout(0);
923
970
  return;
924
971
  }
925
972
 
@@ -928,6 +975,7 @@ export class CommentUiController {
928
975
  const occupiedTops = [];
929
976
  const hoverRegions = [];
930
977
  const showPassiveMarkers = this.shouldRenderPassivePreviewMarkers();
978
+ const previewBubbles = [];
931
979
  groups.forEach((group) => {
932
980
  const target = this.resolvePreviewTarget(group.anchor);
933
981
  if (!target?.bubbleRect) {
@@ -962,7 +1010,7 @@ export class CommentUiController {
962
1010
 
963
1011
  const bubble = document.createElement('button');
964
1012
  bubble.type = 'button';
965
- bubble.className = 'comment-preview-badge';
1013
+ bubble.className = 'ui-state-marker ui-state-marker--comment comment-preview-badge';
966
1014
  bubble.dataset.commentPreviewGroupKeys = group.key;
967
1015
  bubble.classList.toggle('is-active', isActive);
968
1016
  bubble.classList.toggle('is-hovered', isHovered);
@@ -995,9 +1043,17 @@ export class CommentUiController {
995
1043
  });
996
1044
  this.previewLayer?.appendChild(bubble);
997
1045
  occupiedTops.push(bubbleTop);
1046
+ previewBubbles.push(bubble);
998
1047
  });
999
1048
 
1000
1049
  this.previewHoverRegions = hoverRegions;
1050
+ const maxBubbleWidth = previewBubbles.reduce(
1051
+ (maxWidth, bubble) => Math.max(maxWidth, bubble.offsetWidth || COMMENT_PREVIEW_BADGE_MIN_WIDTH),
1052
+ 0,
1053
+ );
1054
+ if (this.syncPreviewRailLayout(maxBubbleWidth)) {
1055
+ this.scheduleLayoutRefresh();
1056
+ }
1001
1057
  if (this.lastPreviewPointerPosition) {
1002
1058
  this.updateHoveredPreviewGroups(
1003
1059
  this.getPreviewGroupKeysAtPoint(this.lastPreviewPointerPosition.x, this.lastPreviewPointerPosition.y),
@@ -1180,7 +1236,7 @@ export class CommentUiController {
1180
1236
  });
1181
1237
 
1182
1238
  const header = document.createElement('div');
1183
- header.className = 'comment-card-header';
1239
+ header.className = 'ui-record-header comment-card-header';
1184
1240
 
1185
1241
  const titleWrap = document.createElement('div');
1186
1242
  titleWrap.className = 'comment-card-title-wrap';
@@ -1197,7 +1253,7 @@ export class CommentUiController {
1197
1253
 
1198
1254
  const closeButton = document.createElement('button');
1199
1255
  closeButton.type = 'button';
1200
- closeButton.className = 'comment-card-close';
1256
+ closeButton.className = 'ui-button ui-button--compact btn btn-ghost ui-pill-button ui-action-pill comment-card-close';
1201
1257
  closeButton.setAttribute('aria-label', 'Close comments');
1202
1258
  closeButton.textContent = 'Close';
1203
1259
  closeButton.addEventListener('click', () => this.closeCard());
@@ -1353,17 +1409,17 @@ export class CommentUiController {
1353
1409
  textarea.placeholder = 'Add context, feedback, or a question...';
1354
1410
 
1355
1411
  const actions = document.createElement('div');
1356
- actions.className = 'comment-card-actions';
1412
+ actions.className = 'ui-record-actions comment-card-actions';
1357
1413
 
1358
1414
  const cancel = document.createElement('button');
1359
1415
  cancel.type = 'button';
1360
- cancel.className = 'btn btn-secondary';
1416
+ cancel.className = 'ui-button btn btn-secondary';
1361
1417
  cancel.textContent = 'Cancel';
1362
1418
  cancel.addEventListener('click', () => this.closeCard());
1363
1419
 
1364
1420
  const submit = document.createElement('button');
1365
1421
  submit.type = 'submit';
1366
- submit.className = 'btn btn-primary';
1422
+ submit.className = 'ui-button btn btn-primary';
1367
1423
  submit.textContent = 'Post comment';
1368
1424
 
1369
1425
  actions.append(cancel, submit);
@@ -1392,7 +1448,7 @@ export class CommentUiController {
1392
1448
  article.className = 'comment-thread-card';
1393
1449
 
1394
1450
  const header = document.createElement('div');
1395
- header.className = 'comment-thread-card-header';
1451
+ header.className = 'ui-record-header comment-thread-card-header';
1396
1452
 
1397
1453
  const heading = document.createElement('div');
1398
1454
  heading.className = 'comment-thread-card-heading';
@@ -1406,17 +1462,17 @@ export class CommentUiController {
1406
1462
  time.textContent = this.formatTimestamp(thread.createdAt);
1407
1463
 
1408
1464
  const actions = document.createElement('div');
1409
- actions.className = 'comment-thread-card-actions';
1465
+ actions.className = 'ui-record-actions comment-thread-card-actions';
1410
1466
 
1411
1467
  const jump = document.createElement('button');
1412
1468
  jump.type = 'button';
1413
- jump.className = 'comment-thread-card-action';
1469
+ jump.className = 'ui-button ui-button--compact btn btn-ghost ui-pill-button ui-action-pill comment-thread-card-action';
1414
1470
  jump.textContent = 'Jump';
1415
1471
  jump.addEventListener('click', () => this.onNavigateToLine?.(thread.anchor?.startLine ?? 1));
1416
1472
 
1417
1473
  const reply = document.createElement('button');
1418
1474
  reply.type = 'button';
1419
- reply.className = 'comment-thread-card-action';
1475
+ reply.className = 'ui-button ui-button--compact btn btn-ghost ui-pill-button ui-action-pill comment-thread-card-action';
1420
1476
  const isReplying = this.activeCard?.replyThreadId === thread.id;
1421
1477
  reply.classList.toggle('is-active', isReplying);
1422
1478
  reply.textContent = 'Reply';
@@ -1433,7 +1489,7 @@ export class CommentUiController {
1433
1489
 
1434
1490
  const resolve = document.createElement('button');
1435
1491
  resolve.type = 'button';
1436
- resolve.className = 'comment-thread-card-action is-danger';
1492
+ resolve.className = 'ui-button ui-button--compact btn btn-ghost ui-pill-button ui-action-pill comment-thread-card-action is-danger';
1437
1493
  resolve.textContent = 'Resolve';
1438
1494
  resolve.addEventListener('click', async () => {
1439
1495
  await this.onResolveThread?.(thread.id);
@@ -1461,7 +1517,7 @@ export class CommentUiController {
1461
1517
  container.className = 'comment-message-card';
1462
1518
 
1463
1519
  const meta = document.createElement('div');
1464
- meta.className = 'comment-message-card-meta';
1520
+ meta.className = 'ui-record-meta comment-message-card-meta';
1465
1521
 
1466
1522
  const author = document.createElement('span');
1467
1523
  author.className = 'comment-message-card-author';
@@ -1495,7 +1551,7 @@ export class CommentUiController {
1495
1551
  (message.reactions ?? []).forEach((reaction) => {
1496
1552
  const chip = document.createElement('button');
1497
1553
  chip.type = 'button';
1498
- chip.className = 'comment-reaction-chip';
1554
+ chip.className = 'ui-chip-button ui-chip-button--comment comment-reaction-chip';
1499
1555
  chip.classList.toggle('is-active', hasLocalReaction(reaction, localUserId));
1500
1556
  chip.setAttribute('aria-pressed', String(hasLocalReaction(reaction, localUserId)));
1501
1557
  chip.title = reaction.users?.map((user) => user.userName).join(', ') || reaction.emoji;
@@ -1529,7 +1585,7 @@ export class CommentUiController {
1529
1585
 
1530
1586
  const moreButton = document.createElement('button');
1531
1587
  moreButton.type = 'button';
1532
- moreButton.className = 'comment-reaction-more-trigger';
1588
+ moreButton.className = 'ui-chip-button ui-chip-button--comment comment-reaction-more-trigger';
1533
1589
  moreButton.dataset.reactionPickerToggle = 'true';
1534
1590
  moreButton.setAttribute('aria-expanded', String(
1535
1591
  isReactionPickerOpen(this.reactionPicker, thread.id, message.id),
@@ -1560,7 +1616,7 @@ export class CommentUiController {
1560
1616
  createQuickReactionButton(thread, message, emoji) {
1561
1617
  const button = document.createElement('button');
1562
1618
  button.type = 'button';
1563
- button.className = 'comment-reaction-quick-add';
1619
+ button.className = 'ui-icon-chip ui-icon-chip--comment comment-reaction-quick-add';
1564
1620
  button.textContent = emoji;
1565
1621
  button.title = `React with ${emoji}`;
1566
1622
  button.addEventListener('click', async () => {
@@ -1585,7 +1641,7 @@ export class CommentUiController {
1585
1641
  createReactionPickerButton(thread, message, emoji) {
1586
1642
  const button = document.createElement('button');
1587
1643
  button.type = 'button';
1588
- button.className = 'comment-reaction-picker-btn';
1644
+ button.className = 'ui-icon-chip ui-icon-chip--comment comment-reaction-picker-btn';
1589
1645
  button.textContent = emoji;
1590
1646
  button.title = `React with ${emoji}`;
1591
1647
  button.addEventListener('click', async () => {
@@ -1609,11 +1665,11 @@ export class CommentUiController {
1609
1665
  textarea.placeholder = 'Reply to thread...';
1610
1666
 
1611
1667
  const actions = document.createElement('div');
1612
- actions.className = 'comment-card-actions';
1668
+ actions.className = 'ui-record-actions comment-card-actions';
1613
1669
 
1614
1670
  const cancel = document.createElement('button');
1615
1671
  cancel.type = 'button';
1616
- cancel.className = 'btn btn-secondary';
1672
+ cancel.className = 'ui-button btn btn-secondary';
1617
1673
  cancel.textContent = 'Cancel';
1618
1674
  cancel.addEventListener('click', () => {
1619
1675
  this.activeCard = {
@@ -1625,7 +1681,7 @@ export class CommentUiController {
1625
1681
 
1626
1682
  const submit = document.createElement('button');
1627
1683
  submit.type = 'submit';
1628
- submit.className = 'btn btn-primary';
1684
+ submit.className = 'ui-button btn btn-primary';
1629
1685
  submit.textContent = 'Reply';
1630
1686
 
1631
1687
  actions.append(cancel, submit);
@@ -463,7 +463,7 @@ export class CommentsPanel {
463
463
 
464
464
  const cancelButton = document.createElement('button');
465
465
  cancelButton.type = 'button';
466
- cancelButton.className = 'btn btn-secondary';
466
+ cancelButton.className = 'ui-button btn btn-secondary';
467
467
  cancelButton.textContent = 'Cancel';
468
468
  cancelButton.addEventListener('click', () => {
469
469
  this.replyDraftThreadId = null;
@@ -472,7 +472,7 @@ export class CommentsPanel {
472
472
 
473
473
  const submitButton = document.createElement('button');
474
474
  submitButton.type = 'submit';
475
- submitButton.className = 'btn btn-primary';
475
+ submitButton.className = 'ui-button btn btn-primary';
476
476
  submitButton.textContent = 'Reply';
477
477
 
478
478
  actions.append(cancelButton, submitButton);