collabmd 0.1.29 → 0.1.30

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 (166) hide show
  1. package/README.md +5 -0
  2. package/dist/client/assets/{CodeMirrorEditor-UCMRAMCV-9k1Shd4H.js → CodeMirrorEditor-UCMRAMCV-tj0_ycIx.js} +1 -1
  3. package/dist/client/assets/architectureDiagram-2XIMDMQ5-BPG5b65m.js +36 -0
  4. package/dist/client/assets/base-CaIEJUF9.css +1 -0
  5. package/dist/client/assets/{blockDiagram-WCTKOSBZ-0rR9ivwb.js → blockDiagram-WCTKOSBZ-F38TRzI3.js} +1 -1
  6. package/dist/client/assets/{chunk-ETSVUAFD-CN-gPl_j.js → chunk-AXZTUZBK-Cxfm7hmm.js} +1 -1
  7. package/dist/client/assets/{chunk-GLR3WWYH-ZnuaBvXp.js → chunk-GLR3WWYH-CQ1LryrI.js} +2 -2
  8. package/dist/client/assets/{chunk-NQ4KR5QH-BCjGNd0g.js → chunk-NQ4KR5QH-BBX89uUa.js} +1 -1
  9. package/dist/client/assets/chunk-NU2UP6EN-CGeKI3BV.js +35 -0
  10. package/dist/client/assets/{chunk-WL4C6EOR-OuH0lN6M.js → chunk-WL4C6EOR-zSV-8h7F.js} +1 -1
  11. package/dist/client/assets/classDiagram-VBA2DB6C-BQqG4q1b.js +1 -0
  12. package/dist/client/assets/classDiagram-v2-RAHNMMFH-BUKbprX1.js +1 -0
  13. package/dist/client/assets/dagre-KLK3FWXG-CAG8WeES.js +4 -0
  14. package/dist/client/assets/diagram-E7M64L7V-Bm5FD0I5.js +24 -0
  15. package/dist/client/assets/diagram-IFDJBPK2-pqicH-av.js +43 -0
  16. package/dist/client/assets/diagram-P4PSJMXO-DwIXO27H.js +24 -0
  17. package/dist/client/assets/dist-3bQB_we3.js +1 -0
  18. package/dist/client/assets/dist-3uAMa5w5.js +1 -0
  19. package/dist/client/assets/{dist-D9c0A4Uh.js → dist-B-02vKOA.js} +1 -1
  20. package/dist/client/assets/{dist-A0eSMIyy.js → dist-BLAlZer_.js} +1 -1
  21. package/dist/client/assets/dist-BVB8rn7o.js +1 -0
  22. package/dist/client/assets/{dist-DveOCUUJ.js → dist-Bc2ND6i3.js} +1 -1
  23. package/dist/client/assets/{dist-BNcU46Ik.js → dist-BqQjnEWr.js} +1 -1
  24. package/dist/client/assets/{dist-Bay3vk5z.js → dist-BtnXjRx0.js} +1 -1
  25. package/dist/client/assets/{dist-CEYpWxod.js → dist-BzbYwa8m.js} +1 -1
  26. package/dist/client/assets/{dist-Ct1bmQnL.js → dist-BzpfHRaE.js} +1 -1
  27. package/dist/client/assets/{dist-BbKpzsTB.js → dist-CGV1S_as.js} +1 -1
  28. package/dist/client/assets/dist-CL-jjdvG.js +1 -0
  29. package/dist/client/assets/{dist-D0e6Qsad.js → dist-CXpKYW9s.js} +1 -1
  30. package/dist/client/assets/{dist-BchrxlnW.js → dist-CjII8VDS.js} +1 -1
  31. package/dist/client/assets/{dist-DnrO-JZd.js → dist-CxfohGIV.js} +1 -1
  32. package/dist/client/assets/{dist-CMHIjMVe.js → dist-D5QOYy7y.js} +1 -1
  33. package/dist/client/assets/{dist-CEtbxXlI.js → dist-DIi0uqLJ.js} +1 -1
  34. package/dist/client/assets/dist-DJ16N7ol.js +1 -0
  35. package/dist/client/assets/dist-DUvaQ-Bj.js +1 -0
  36. package/dist/client/assets/{dist-DnxPQ0-w.js → dist-DWJL3_bF.js} +6 -6
  37. package/dist/client/assets/dist-DsL7sqZR.js +1 -0
  38. package/dist/client/assets/{dist-DbhQ5ANS.js → dist-Dyt7n5Ui.js} +1 -1
  39. package/dist/client/assets/{dist-BOrlJDrR.js → dist-E_jPb93e.js} +1 -1
  40. package/dist/client/assets/{dist-D-gRgZ6e.js → dist-O1z0Ayg0.js} +1 -1
  41. package/dist/client/assets/dist-OQE68wIX.js +1 -0
  42. package/dist/client/assets/dist-k47-Io7D.js +1 -0
  43. package/dist/client/assets/{dist-Cdzvi8cf.js → dist-lDhnP-K1.js} +1 -1
  44. package/dist/client/assets/{dist-DS6mEQYy.js → dist-rMhng5DK.js} +1 -1
  45. package/dist/client/assets/dist-xO1ImhKz.js +1 -0
  46. package/dist/client/assets/{drawio-editor-CgbaOvpF.js → drawio-editor-CTZtMo1J.js} +1 -1
  47. package/dist/client/assets/{drawioEditor-Eb2yYhjE.js → drawioEditor-keSJVTcv.js} +2 -2
  48. package/dist/client/assets/editor-session-C9l9XwVg.js +16 -0
  49. package/dist/client/assets/{embedded-editor-base-DXRHYz6R.css → embedded-editor-base-DgmRcsR8.css} +1 -1
  50. package/dist/client/assets/{en-CTDJDZ6K-BHbXrqU0.js → en-IQZW2TQM-CZeIyvu2.js} +1 -1
  51. package/dist/client/assets/{erDiagram-INFDFZHY-DP3f20BF.js → erDiagram-INFDFZHY-DlhPv4az.js} +1 -1
  52. package/dist/client/assets/{excalidraw-editor-Dj0c0PO6.js → excalidraw-editor-CBEWfuhb.js} +2 -2
  53. package/dist/client/assets/excalidraw-editor-Dj3i59LW.css +1 -0
  54. package/dist/client/assets/excalidraw-scene-BL-OtvCd.js +176 -0
  55. package/dist/client/assets/excalidrawEditor-Ws-fwGOc.js +2 -0
  56. package/dist/client/assets/{export-page-CUOMczdp.js → export-page-B6xYnwy7.js} +2 -2
  57. package/dist/client/assets/exportDocument-8JBw0p8O.js +2 -0
  58. package/dist/client/assets/{exportDocument-CLPYN_gg.css → exportDocument-CtX_4PSk.css} +1 -1
  59. package/dist/client/assets/{flowDiagram-PKNHOUZH-CRbK5_5h.js → flowDiagram-PKNHOUZH-BU4MOSq8.js} +1 -1
  60. package/dist/client/assets/ganttDiagram-A5KZAMGK-CbX67lZu.js +292 -0
  61. package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-DhyvWcW1.js +65 -0
  62. package/dist/client/assets/image-XLNPCCAD-B4wZgh-Y.js +1 -0
  63. package/dist/client/assets/{index-CVgVzuh6.js → index-CywRKxsk.js} +2 -2
  64. package/dist/client/assets/index-R4OArFku.css +1 -0
  65. package/dist/client/assets/infoDiagram-LFFYTUFH-Bap6MyHX.js +2 -0
  66. package/dist/client/assets/{ishikawaDiagram-PHBUUO56-BDVPgkv4.js → ishikawaDiagram-PHBUUO56-DbftskqN.js} +1 -1
  67. package/dist/client/assets/{journeyDiagram-4ABVD52K-DtNnwL48.js → journeyDiagram-4ABVD52K-165YoX6B.js} +1 -1
  68. package/dist/client/assets/{jsx-runtime-B8dyIONw.js → jsx-runtime-DiaMmmG6.js} +1 -1
  69. package/dist/client/assets/{kanban-definition-K7BYSVSG-C565sUNo.js → kanban-definition-K7BYSVSG-MlixoNyx.js} +1 -1
  70. package/dist/client/assets/{main-C8adLQnt.js → main-CB97rY6B.js} +172 -133
  71. package/dist/client/assets/mermaid-parser.core-lf87rdG-.js +4 -0
  72. package/dist/client/assets/{mermaid-runtime-DqncQoYK.js → mermaid-runtime-C4a7TrTS.js} +3 -3
  73. package/dist/client/assets/{mindmap-definition-YRQLILUH-BbYzGfTD.js → mindmap-definition-YRQLILUH-_YbQSc4Q.js} +1 -1
  74. package/dist/client/assets/pieDiagram-SKSYHLDU-CqCgXFom.js +30 -0
  75. package/dist/client/assets/preview-render-compiler-TmHpBDlh.js +1 -0
  76. package/dist/client/assets/preview-render-compiler-aYpnXmNm.js +35 -0
  77. package/dist/client/assets/preview-render-worker-BVbVXXPS.js +59 -0
  78. package/dist/client/assets/{quadrantDiagram-337W2JSQ-DhlKLozd.js → quadrantDiagram-337W2JSQ-B8eMuhg4.js} +1 -1
  79. package/dist/client/assets/{requirementDiagram-Z7DCOOCP-B7ZOMsIb.js → requirementDiagram-Z7DCOOCP-B6POvqz4.js} +1 -1
  80. package/dist/client/assets/{room-B0UGbzQj.js → room-COWSP--i.js} +1 -1
  81. package/dist/client/assets/stateDiagram-RAJIS63D-DbewhqwJ.js +1 -0
  82. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-DbW-jVkS.js +1 -0
  83. package/dist/client/assets/{vennDiagram-LZ73GAT5-Cs9aQmVM.js → vennDiagram-LZ73GAT5-C1hrSbf2.js} +1 -1
  84. package/dist/client/assets/{xychartDiagram-JWTSCODW-B40ybbzC.js → xychartDiagram-JWTSCODW-DhpWp8LV.js} +1 -1
  85. package/dist/client/assets/{yjs-provider-reset-guard-DUP9XupO.js → yjs-provider-reset-guard-DWGhQClh.js} +1 -1
  86. package/dist/client/drawio-editor.html +1 -1
  87. package/dist/client/excalidraw-editor.html +1 -1
  88. package/dist/client/export-document.html +4 -4
  89. package/dist/client/index.html +4 -4
  90. package/docker-compose.yml +1 -0
  91. package/package.json +23 -21
  92. package/src/client/application/app-shell/ui-feature-shell.js +47 -1
  93. package/src/client/application/app-shell/ui-feature-toolbar.js +25 -3
  94. package/src/client/application/app-shell-elements.js +2 -0
  95. package/src/client/application/diagram-preview-hydrator.js +1 -1
  96. package/src/client/application/markdown-frontmatter.js +152 -0
  97. package/src/client/application/preview-render-compiler.js +10 -1
  98. package/src/client/application/preview-render-executor.js +8 -1
  99. package/src/client/application/preview-render-worker.js +4 -0
  100. package/src/client/application/preview-renderer.js +51 -1
  101. package/src/client/application/workspace-preview-controller.js +1 -0
  102. package/src/client/bootstrap/collabmd-app-shell.js +5 -0
  103. package/src/client/domain/markdown-formatting.js +1 -1
  104. package/src/client/domain/markdown-toolbar-actions.js +36 -0
  105. package/src/client/export/export-pipeline.js +3 -1
  106. package/src/client/infrastructure/editor-session.js +4 -0
  107. package/src/client/infrastructure/editor-view-adapter.js +36 -2
  108. package/src/client/infrastructure/excalidraw-room-client.js +12 -4
  109. package/src/client/presentation/app-shell-template.js +15 -0
  110. package/src/client/presentation/file-action-controller.js +1 -2
  111. package/src/client/presentation/git-diff-view-controller.js +7 -10
  112. package/src/client/styles/base.css +5 -0
  113. package/src/client/styles/features/preview-markdown.css +138 -0
  114. package/src/client/styles/layout/editor-page.css +6 -0
  115. package/src/client/styles/layout/responsive.css +89 -0
  116. package/src/client/styles/layout/shell.css +6 -0
  117. package/src/client/styles/overrides/codemirror.css +74 -0
  118. package/src/server/auth/create-auth-service.js +32 -6
  119. package/src/server/config/env.js +27 -0
  120. package/src/server/config/perf-logging.js +39 -0
  121. package/src/server/create-app-server.js +102 -5
  122. package/src/server/domain/backlink-index.js +31 -7
  123. package/src/server/domain/collaboration/collaboration-room.js +28 -5
  124. package/src/server/infrastructure/git/history-service.js +1 -1
  125. package/src/server/infrastructure/persistence/pull-backup-store.js +1 -1
  126. package/src/server/infrastructure/persistence/vault-file-store.js +74 -31
  127. package/src/server/infrastructure/workspace/file-system-sync-service.js +46 -12
  128. package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +32 -7
  129. package/src/server/shared/async-utils.js +25 -0
  130. package/dist/client/assets/architectureDiagram-2XIMDMQ5-Bhh8YQHn.js +0 -36
  131. package/dist/client/assets/base-B5gONpJK.css +0 -1
  132. package/dist/client/assets/chunk-27N2SPOM-DbHKsQ_j.js +0 -34
  133. package/dist/client/assets/classDiagram-VBA2DB6C-Bpdym3dj.js +0 -1
  134. package/dist/client/assets/classDiagram-v2-RAHNMMFH-CRAsdVDB.js +0 -1
  135. package/dist/client/assets/dagre-KLK3FWXG-CXSnjrX-.js +0 -4
  136. package/dist/client/assets/diagram-E7M64L7V-De0StFef.js +0 -24
  137. package/dist/client/assets/diagram-IFDJBPK2-B2m6AJbU.js +0 -43
  138. package/dist/client/assets/diagram-P4PSJMXO-YBWLVENw.js +0 -24
  139. package/dist/client/assets/dist-BUEzOS23.js +0 -1
  140. package/dist/client/assets/dist-BflL-_8D.js +0 -1
  141. package/dist/client/assets/dist-CI_qEj5K.js +0 -1
  142. package/dist/client/assets/dist-CtFNDttZ.js +0 -1
  143. package/dist/client/assets/dist-DS_4VpEd.js +0 -1
  144. package/dist/client/assets/dist-DwsFUbuB.js +0 -1
  145. package/dist/client/assets/dist-VCYAp958.js +0 -1
  146. package/dist/client/assets/dist-Yxt8txtM.js +0 -1
  147. package/dist/client/assets/dist-npxgF_6V.js +0 -1
  148. package/dist/client/assets/dist-u0zDRMQt.js +0 -1
  149. package/dist/client/assets/editor-session-DFzBn4RY.js +0 -16
  150. package/dist/client/assets/excalidraw-editor-Bat1FRoT.css +0 -1
  151. package/dist/client/assets/excalidraw-scene-CixfM-jL.js +0 -174
  152. package/dist/client/assets/excalidrawEditor-diK4VITg.js +0 -2
  153. package/dist/client/assets/exportDocument-BM4_f2n8.js +0 -2
  154. package/dist/client/assets/ganttDiagram-A5KZAMGK-R-AMIgxR.js +0 -292
  155. package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-CIPit_Sh.js +0 -65
  156. package/dist/client/assets/image-32MOCVDX-BB6utPzr.js +0 -1
  157. package/dist/client/assets/index-BOOhEiLo.css +0 -1
  158. package/dist/client/assets/infoDiagram-LFFYTUFH-CF8jXFTm.js +0 -2
  159. package/dist/client/assets/mermaid-parser.core-By_5k2SF.js +0 -4
  160. package/dist/client/assets/pieDiagram-SKSYHLDU-CAU_gC7U.js +0 -30
  161. package/dist/client/assets/preview-render-compiler-CVfDHnqM.js +0 -1
  162. package/dist/client/assets/preview-render-compiler-CapHJDHT.js +0 -1
  163. package/dist/client/assets/preview-render-worker-wMS710Bv.js +0 -23
  164. package/dist/client/assets/stateDiagram-RAJIS63D-CPoSrNwn.js +0 -1
  165. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-5GW0Y21a.js +0 -1
  166. /package/dist/client/assets/{base-r5Zg2n3k.js → base-h1uscjTi.js} +0 -0
@@ -242,6 +242,14 @@ export function appShellTemplate() {
242
242
  <span class="ui-toolbar-button-label">Share</span>
243
243
  </button>
244
244
 
245
+ <button class=${buttonClassNames({ variant: 'ghost', toolbar: true, extra: 'toolbar-search-files-btn' })} id="searchFilesBtn" aria-label="Search files" title="Search files">
246
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
247
+ <circle cx="11" cy="11" r="7"></circle>
248
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
249
+ </svg>
250
+ <span class="ui-toolbar-button-label">Search files</span>
251
+ </button>
252
+
245
253
  <details class="toolbar-overflow-group hidden" id="exportMenuGroup">
246
254
  <summary class=${buttonClassNames({ variant: 'ghost', toolbar: true, extra: 'toolbar-overflow-group-toggle' })} aria-label="Export options" title="Export">
247
255
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -327,6 +335,13 @@ export function appShellTemplate() {
327
335
  <span class="pane-label-meta" id="lineInfo">Ln 1, Col 1</span>
328
336
  </div>
329
337
  <div class="pane-header-actions">
338
+ <button class=${buttonClassNames({ variant: 'ghost', size: 'compact', toggle: true, extra: 'mobile-editor-find-btn' })} id="editorFindBtn" aria-label="Find in file" title="Find in file">
339
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
340
+ <circle cx="11" cy="11" r="7"></circle>
341
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
342
+ </svg>
343
+ <span class="ui-action-label">Find</span>
344
+ </button>
330
345
  <button class=${buttonClassNames({ variant: 'ghost', size: 'compact', toggle: true, extra: 'hidden' })} id="commentSelectionBtn" aria-label="Comment current line or selection" title="Comment current line or selection">
331
346
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
332
347
  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
@@ -436,11 +436,10 @@ export class FileActionController {
436
436
 
437
437
  this.setActionDialogBusy(true);
438
438
 
439
- let shouldClose = false;
439
+ let shouldClose;
440
440
  try {
441
441
  shouldClose = await this.pendingAction.onSubmit(rawValue);
442
442
  } catch (error) {
443
- shouldClose = false;
444
443
  this.showError('Failed to complete action', error);
445
444
  }
446
445
 
@@ -696,16 +696,13 @@ export class GitDiffViewController {
696
696
  const errorMessage = this.fileErrors.get(file.path);
697
697
  const detail = this.getFileDetail(file.path) ?? file;
698
698
 
699
- let bodyMarkup = '';
700
- if (isCollapsed) {
701
- bodyMarkup = '';
702
- } else if (isLoading) {
703
- bodyMarkup = '<div class="diff-empty-state">Loading file diff...</div>';
704
- } else if (errorMessage) {
705
- bodyMarkup = `<div class="diff-empty-state">${escapeHtml(errorMessage)}</div>`;
706
- } else {
707
- bodyMarkup = this.renderDiffDetail(detail, index, { includeHeader: false });
708
- }
699
+ const bodyMarkup = isCollapsed
700
+ ? ''
701
+ : isLoading
702
+ ? '<div class="diff-empty-state">Loading file diff...</div>'
703
+ : errorMessage
704
+ ? `<div class="diff-empty-state">${escapeHtml(errorMessage)}</div>`
705
+ : this.renderDiffDetail(detail, index, { includeHeader: false });
709
706
 
710
707
  return `
711
708
  <section
@@ -11,10 +11,15 @@ html {
11
11
  scroll-behavior: smooth;
12
12
  hanging-punctuation: first last;
13
13
  scroll-padding-top: var(--space-16);
14
+ height: 100%;
15
+ overflow: hidden;
16
+ overscroll-behavior: none;
14
17
  }
15
18
 
16
19
  body {
20
+ height: 100%;
17
21
  min-height: 100dvh;
22
+ overflow: hidden;
18
23
  line-height: 1.6;
19
24
  font-family: var(--font-body, sans-serif);
20
25
  font-size: var(--text-sm);
@@ -13,6 +13,10 @@
13
13
  margin-top: 0;
14
14
  }
15
15
 
16
+ .preview-content .frontmatter-block + h1 {
17
+ margin-top: 0;
18
+ }
19
+
16
20
  .preview-content h2 {
17
21
  margin: var(--space-6) 0 var(--space-3);
18
22
  font-size: var(--text-lg);
@@ -40,6 +44,128 @@
40
44
  margin-bottom: var(--space-4);
41
45
  }
42
46
 
47
+ .preview-content .frontmatter-block {
48
+ max-width: 72ch;
49
+ margin: 0 0 var(--space-5);
50
+ padding: var(--space-4);
51
+ border: 1px solid var(--color-border);
52
+ border-radius: var(--radius-lg);
53
+ background: color-mix(in oklab, var(--color-surface-offset) 72%, var(--color-surface));
54
+ }
55
+
56
+ .preview-content .frontmatter-label {
57
+ color: var(--color-text-muted);
58
+ font-size: var(--text-ui-sm);
59
+ font-weight: 600;
60
+ letter-spacing: 0.04em;
61
+ text-transform: uppercase;
62
+ }
63
+
64
+ .preview-content .frontmatter-block > .frontmatter-label {
65
+ display: block;
66
+ margin-bottom: var(--space-3);
67
+ }
68
+
69
+ .preview-content .frontmatter-header {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: space-between;
73
+ gap: var(--space-3);
74
+ margin-bottom: var(--space-3);
75
+ }
76
+
77
+ .preview-content .frontmatter-toggle {
78
+ flex-shrink: 0;
79
+ min-height: 30px;
80
+ padding: 0 var(--space-3);
81
+ border: 1px solid var(--color-border);
82
+ border-radius: 999px;
83
+ background: var(--color-surface);
84
+ color: var(--color-text);
85
+ font: inherit;
86
+ font-size: var(--text-ui-sm);
87
+ cursor: pointer;
88
+ }
89
+
90
+ .preview-content .frontmatter-toggle:hover {
91
+ background: var(--color-surface-offset);
92
+ }
93
+
94
+ .preview-content .frontmatter-toggle:focus-visible {
95
+ outline: 2px solid var(--color-primary);
96
+ outline-offset: 2px;
97
+ }
98
+
99
+ .preview-content .frontmatter-summary {
100
+ margin-bottom: var(--space-1);
101
+ color: var(--color-text-muted);
102
+ font-size: var(--text-ui-sm);
103
+ }
104
+
105
+ .preview-content .frontmatter-properties {
106
+ display: grid;
107
+ gap: var(--space-3);
108
+ margin: 0;
109
+ }
110
+
111
+ .preview-content .frontmatter-property {
112
+ display: grid;
113
+ grid-template-columns: minmax(0, 160px) minmax(0, 1fr);
114
+ gap: var(--space-3);
115
+ align-items: start;
116
+ }
117
+
118
+ .preview-content .frontmatter-key {
119
+ margin: 0;
120
+ color: var(--color-text-muted);
121
+ font-size: var(--text-ui-sm);
122
+ font-weight: 600;
123
+ }
124
+
125
+ .preview-content .frontmatter-value {
126
+ min-width: 0;
127
+ margin: 0;
128
+ }
129
+
130
+ .preview-content .frontmatter-value-text {
131
+ display: inline-block;
132
+ color: var(--color-text);
133
+ overflow-wrap: anywhere;
134
+ }
135
+
136
+ .preview-content .frontmatter-value-list {
137
+ display: flex;
138
+ flex-wrap: wrap;
139
+ gap: var(--space-2);
140
+ }
141
+
142
+ .preview-content .frontmatter-value-pill {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ min-height: 28px;
146
+ padding: 0 var(--space-3);
147
+ border: 1px solid var(--color-border);
148
+ border-radius: 999px;
149
+ background: var(--color-surface);
150
+ color: var(--color-text);
151
+ font-size: var(--text-ui-sm);
152
+ }
153
+
154
+ .preview-content .frontmatter-value-code {
155
+ margin: 0;
156
+ border-radius: var(--radius-md);
157
+ }
158
+
159
+ .preview-content .frontmatter-value-code code {
160
+ white-space: pre-wrap;
161
+ word-break: break-word;
162
+ }
163
+
164
+ .preview-content .frontmatter-empty {
165
+ color: var(--color-text-muted);
166
+ font-size: var(--text-ui-sm);
167
+ }
168
+
43
169
  .preview-content p:has(> .excalidraw-embed-placeholder:only-child),
44
170
  .preview-content p:has(> .video-embed-placeholder:only-child),
45
171
  .preview-content p:has(> .plantuml-shell:only-child) {
@@ -202,3 +328,15 @@
202
328
  border-radius: var(--radius-md);
203
329
  cursor: zoom-in;
204
330
  }
331
+
332
+ @media (max-width: 768px) {
333
+ .preview-content .frontmatter-header {
334
+ align-items: flex-start;
335
+ flex-direction: column;
336
+ }
337
+
338
+ .preview-content .frontmatter-property {
339
+ grid-template-columns: 1fr;
340
+ gap: var(--space-1);
341
+ }
342
+ }
@@ -316,6 +316,12 @@
316
316
  border-left: 1px solid var(--color-divider);
317
317
  }
318
318
 
319
+ .markdown-toolbar .markdown-toolbar-group--mobile-only,
320
+ .pane-header-actions .mobile-editor-find-btn,
321
+ .toolbar-overflow-menu .toolbar-search-files-btn {
322
+ display: none !important;
323
+ }
324
+
319
325
  .markdown-toolbar-block-trigger {
320
326
  display: inline-flex;
321
327
  align-items: center;
@@ -130,6 +130,10 @@
130
130
  width: 100%;
131
131
  }
132
132
 
133
+ .toolbar-overflow-menu .toolbar-search-files-btn {
134
+ display: inline-flex !important;
135
+ }
136
+
133
137
  .toolbar-overflow-menu .ui-toolbar-button-label {
134
138
  display: inline;
135
139
  min-width: 0;
@@ -204,6 +208,53 @@
204
208
  flex: 1;
205
209
  }
206
210
 
211
+ .editor-pane {
212
+ position: relative;
213
+ }
214
+
215
+ .editor-pane > .pane-header {
216
+ position: sticky;
217
+ top: 0;
218
+ z-index: 8;
219
+ gap: var(--space-2);
220
+ padding-inline: var(--space-2);
221
+ }
222
+
223
+ .editor-pane > .pane-header .pane-header-meta {
224
+ flex: 1 1 auto;
225
+ gap: var(--space-2);
226
+ min-width: 0;
227
+ overflow: hidden;
228
+ }
229
+
230
+ .editor-pane > .pane-header .pane-label {
231
+ flex-shrink: 0;
232
+ }
233
+
234
+ .editor-pane > .pane-header .pane-label-meta {
235
+ min-width: 0;
236
+ overflow: hidden;
237
+ text-overflow: ellipsis;
238
+ white-space: nowrap;
239
+ }
240
+
241
+ .editor-pane > .pane-header .pane-header-actions {
242
+ flex-shrink: 0;
243
+ gap: 0;
244
+ min-width: 0;
245
+ margin-left: auto;
246
+ }
247
+
248
+ .editor-pane > .pane-header .ui-button--compact {
249
+ gap: 4px;
250
+ padding-inline: 6px;
251
+ }
252
+
253
+ .editor-pane > .pane-header .ui-action-label,
254
+ .editor-pane > .pane-header #wrapToggleLabel {
255
+ font-size: 11px;
256
+ }
257
+
207
258
  .preview-content {
208
259
  padding: var(--space-4);
209
260
  }
@@ -283,15 +334,29 @@
283
334
  }
284
335
 
285
336
  .markdown-toolbar {
337
+ position: sticky;
338
+ top: 28px;
339
+ z-index: 7;
286
340
  overflow-x: auto;
287
341
  flex-wrap: nowrap;
288
342
  scrollbar-width: thin;
289
343
  }
290
344
 
345
+ .markdown-toolbar .markdown-toolbar-group--mobile-only {
346
+ display: inline-flex !important;
347
+ margin-left: 0;
348
+ padding-left: 0;
349
+ border-left: none;
350
+ }
351
+
291
352
  .markdown-toolbar-popover {
292
353
  max-width: min(240px, calc(100vw - 24px));
293
354
  }
294
355
 
356
+ .pane-header-actions .mobile-editor-find-btn {
357
+ display: inline-flex !important;
358
+ }
359
+
295
360
  .outline-panel {
296
361
  position: absolute;
297
362
  inset: 0;
@@ -403,6 +468,30 @@
403
468
  }
404
469
  }
405
470
 
471
+ @media (max-width: 480px) {
472
+ .editor-pane > .pane-header {
473
+ gap: var(--space-1);
474
+ padding-inline: var(--space-1);
475
+ }
476
+
477
+ .editor-pane > .pane-header .pane-header-meta {
478
+ gap: var(--space-1);
479
+ }
480
+
481
+ .editor-pane > .pane-header .pane-label-meta {
482
+ font-size: 11px;
483
+ }
484
+
485
+ .editor-pane > .pane-header .ui-button--compact {
486
+ padding-inline: 4px;
487
+ }
488
+
489
+ .editor-pane > .pane-header .ui-action-label,
490
+ .editor-pane > .pane-header #wrapToggleLabel {
491
+ font-size: 10px;
492
+ }
493
+ }
494
+
406
495
  @media (min-width: 769px) {
407
496
  .mobile-view-toggle {
408
497
  display: none !important;
@@ -1,8 +1,14 @@
1
1
  /* ===== APP SHELL ===== */
2
2
 
3
3
  .app-shell {
4
+ position: fixed;
5
+ top: var(--app-viewport-offset-top, 0px);
6
+ right: 0;
7
+ left: 0;
4
8
  display: flex;
9
+ width: 100%;
5
10
  height: 100dvh;
11
+ height: var(--app-viewport-height, 100dvh);
6
12
  overflow: hidden;
7
13
  }
8
14
 
@@ -51,3 +51,77 @@
51
51
  content: '📄';
52
52
  font-size: var(--text-ui-sm);
53
53
  }
54
+
55
+ .cm-panels-top {
56
+ border-bottom: 1px solid var(--color-divider);
57
+ }
58
+
59
+ .cm-panel.cm-search {
60
+ gap: var(--space-2);
61
+ align-items: center;
62
+ padding: var(--space-2);
63
+ background: oklch(from var(--color-surface) l c h / 0.98);
64
+ color: var(--color-text);
65
+ box-shadow: inset 0 -1px 0 var(--color-divider);
66
+ font-family: var(--font-body);
67
+ }
68
+
69
+ .cm-panel.cm-search label {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ gap: 6px;
73
+ color: var(--color-text-muted);
74
+ font-size: var(--text-xs);
75
+ font-weight: 600;
76
+ }
77
+
78
+ .cm-panel.cm-search .cm-textfield {
79
+ min-height: 32px;
80
+ padding: 0 var(--space-2);
81
+ border: 1px solid var(--color-border);
82
+ border-radius: var(--radius-sm);
83
+ background: var(--color-surface-offset);
84
+ color: var(--color-text);
85
+ font-family: var(--font-body);
86
+ font-size: var(--text-sm);
87
+ }
88
+
89
+ .cm-panel.cm-search .cm-button {
90
+ min-height: 32px;
91
+ padding: 0 10px;
92
+ border: 1px solid var(--color-border);
93
+ border-radius: var(--radius-sm);
94
+ background: var(--color-surface-offset);
95
+ color: var(--color-text);
96
+ font-family: var(--font-body);
97
+ font-size: var(--text-xs);
98
+ font-weight: 600;
99
+ }
100
+
101
+ .cm-panel.cm-search .cm-button:hover:not(:disabled),
102
+ .cm-panel.cm-search .cm-button:focus-visible {
103
+ background: var(--color-surface-dynamic);
104
+ outline: none;
105
+ }
106
+
107
+ @media (max-width: 768px) {
108
+ .cm-panel.cm-search {
109
+ flex-wrap: wrap;
110
+ align-items: stretch;
111
+ }
112
+
113
+ .cm-panel.cm-search label {
114
+ flex: 1 1 100%;
115
+ }
116
+
117
+ .cm-panel.cm-search .cm-textfield {
118
+ width: 100%;
119
+ min-height: 40px;
120
+ font-size: 16px;
121
+ }
122
+
123
+ .cm-panel.cm-search .cm-button {
124
+ min-height: 40px;
125
+ padding: 0 12px;
126
+ }
127
+ }
@@ -168,8 +168,7 @@ function readAuthenticatedOidcSession(sessionCookieManager, req) {
168
168
  return null;
169
169
  }
170
170
 
171
- const expiresAt = Number(session.expiresAt);
172
- if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
171
+ if (hasExpiredSession(session)) {
173
172
  return null;
174
173
  }
175
174
 
@@ -185,6 +184,29 @@ function readAuthenticatedOidcSession(sessionCookieManager, req) {
185
184
  };
186
185
  }
187
186
 
187
+ function hasExpiredSession(session) {
188
+ const expiresAt = Number(session?.expiresAt);
189
+ return Number.isFinite(expiresAt) && expiresAt <= Date.now();
190
+ }
191
+
192
+ function resolveSessionExpiresAt(sessionMaxAgeMs, fallbackExpiresAt = null) {
193
+ if (Number.isFinite(sessionMaxAgeMs) && sessionMaxAgeMs > 0) {
194
+ return Date.now() + sessionMaxAgeMs;
195
+ }
196
+
197
+ return Number.isFinite(fallbackExpiresAt) ? fallbackExpiresAt : null;
198
+ }
199
+
200
+ function createSessionCookieOptions(expiresAt) {
201
+ if (!Number.isFinite(expiresAt)) {
202
+ return {};
203
+ }
204
+
205
+ return {
206
+ expires: new Date(expiresAt),
207
+ };
208
+ }
209
+
188
210
  function buildClientConfig(authConfig, { basePath = '' } = {}) {
189
211
  const loginEndpoint = prependBasePath(basePath, '/api/auth/oidc/login');
190
212
  const sessionEndpoint = prependBasePath(basePath, '/api/auth/session');
@@ -232,7 +254,7 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
232
254
 
233
255
  function hasValidSession(req) {
234
256
  const session = sessionCookieManager.readSession(req);
235
- return session?.strategy === AUTH_STRATEGY_PASSWORD;
257
+ return session?.strategy === AUTH_STRATEGY_PASSWORD && !hasExpiredSession(session);
236
258
  }
237
259
 
238
260
  return {
@@ -260,6 +282,8 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
260
282
  }, { kind: 'invalid_credentials' });
261
283
  }
262
284
 
285
+ const expiresAt = resolveSessionExpiresAt(authConfig.sessionMaxAgeMs);
286
+
263
287
  return createResponse(200, {
264
288
  auth: clientConfig,
265
289
  authenticated: true,
@@ -269,8 +293,9 @@ function createPasswordStrategy(authConfig, sessionCookieManager, clientConfig)
269
293
  kind: 'session_created',
270
294
  setCookie: sessionCookieManager.createSessionCookie(req, {
271
295
  authenticatedAt: Date.now(),
296
+ ...(Number.isFinite(expiresAt) ? { expiresAt } : {}),
272
297
  strategy: AUTH_STRATEGY_PASSWORD,
273
- }),
298
+ }, createSessionCookieOptions(expiresAt)),
274
299
  });
275
300
  },
276
301
 
@@ -456,9 +481,10 @@ function createOidcStrategy(authConfig, sessionCookieManager, flowCookieManager,
456
481
  return createErrorRedirect(req, accessDecision.error, flowPayload);
457
482
  }
458
483
 
459
- const expiresAt = Number.isFinite(claims.exp)
484
+ const tokenExpiresAt = Number.isFinite(claims.exp)
460
485
  ? claims.exp * 1000
461
486
  : Date.now() + (Math.max(Number(tokens.expires_in) || 3600, 1) * 1000);
487
+ const expiresAt = resolveSessionExpiresAt(authConfig.sessionMaxAgeMs, tokenExpiresAt);
462
488
  const returnTo = sanitizeReturnTo(flowPayload.returnTo, {
463
489
  basePath,
464
490
  publicBaseUrl: authConfig.oidc.publicBaseUrl,
@@ -480,7 +506,7 @@ function createOidcStrategy(authConfig, sessionCookieManager, flowCookieManager,
480
506
  expiresAt,
481
507
  strategy: AUTH_STRATEGY_OIDC,
482
508
  user,
483
- }),
509
+ }, createSessionCookieOptions(expiresAt)),
484
510
  flowCookieManager.clear(req),
485
511
  ],
486
512
  });
@@ -10,12 +10,34 @@ import {
10
10
  createRandomSessionSecret,
11
11
  } from '../auth/create-auth-service.js';
12
12
  import { loadBuildInfo } from './build-info.js';
13
+ import { isPerfLoggingEnabled } from './perf-logging.js';
13
14
 
14
15
  function parsePositiveInt(rawValue, fallbackValue) {
15
16
  const parsed = Number.parseInt(rawValue ?? '', 10);
16
17
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallbackValue;
17
18
  }
18
19
 
20
+ function parseOptionalPositiveInt(rawValue, {
21
+ fallbackValue = null,
22
+ variableName = 'value',
23
+ } = {}) {
24
+ const normalized = String(rawValue ?? '').trim();
25
+ if (!normalized) {
26
+ return fallbackValue;
27
+ }
28
+
29
+ if (!/^[1-9]\d*$/u.test(normalized)) {
30
+ throw new Error(`${variableName} must be a positive integer.`);
31
+ }
32
+
33
+ const parsed = Number(normalized);
34
+ if (Number.isSafeInteger(parsed) && parsed > 0) {
35
+ return parsed;
36
+ }
37
+
38
+ throw new Error(`${variableName} must be a positive integer.`);
39
+ }
40
+
19
41
  function parsePort(rawPort, fallbackPort) {
20
42
  return parsePositiveInt(rawPort, fallbackPort);
21
43
  }
@@ -284,6 +306,10 @@ export function loadConfig(overrides = {}) {
284
306
  password,
285
307
  passwordWasGenerated,
286
308
  sessionCookieName: authOverrides.sessionCookieName || process.env.AUTH_SESSION_COOKIE_NAME || 'collabmd_auth',
309
+ sessionMaxAgeMs: parseOptionalPositiveInt(
310
+ authOverrides.sessionMaxAgeMs ?? process.env.AUTH_SESSION_MAX_AGE_MS,
311
+ { variableName: 'AUTH_SESSION_MAX_AGE_MS' },
312
+ ),
287
313
  sessionSecret: authOverrides.sessionSecret || process.env.AUTH_SESSION_SECRET || createRandomSessionSecret(),
288
314
  strategy: authStrategy,
289
315
  },
@@ -296,6 +322,7 @@ export function loadConfig(overrides = {}) {
296
322
  httpRequestTimeoutMs: parsePositiveInt(process.env.HTTP_REQUEST_TIMEOUT_MS, 30_000),
297
323
  git,
298
324
  gitEnabled: git.enabled,
325
+ perfLoggingEnabled: overrides.perfLoggingEnabled ?? isPerfLoggingEnabled(process.env.COLLABMD_PERF_LOGGING),
299
326
  port: parsePort(process.env.PORT, 1234),
300
327
  nodeEnv,
301
328
  plantumlServerUrl: process.env.PLANTUML_SERVER_URL || 'https://www.plantuml.com/plantuml',
@@ -0,0 +1,39 @@
1
+ function formatPerfValue(value) {
2
+ if (typeof value === 'number' && Number.isFinite(value)) {
3
+ return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/\.?0+$/u, '');
4
+ }
5
+
6
+ if (typeof value === 'string') {
7
+ return /[\s=]/u.test(value) ? JSON.stringify(value) : value;
8
+ }
9
+
10
+ if (typeof value === 'boolean') {
11
+ return value ? 'true' : 'false';
12
+ }
13
+
14
+ return JSON.stringify(value);
15
+ }
16
+
17
+ export function isPerfLoggingEnabled(value = '') {
18
+ if (typeof value === 'boolean') {
19
+ return value;
20
+ }
21
+
22
+ const normalized = String(value ?? '').trim().toLowerCase();
23
+ return normalized === '1'
24
+ || normalized === 'true'
25
+ || normalized === 'yes'
26
+ || normalized === 'on';
27
+ }
28
+
29
+ export function logPerfEvent(enabled, scope, fields = {}) {
30
+ if (!enabled) {
31
+ return;
32
+ }
33
+
34
+ const formattedFields = Object.entries(fields)
35
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
36
+ .map(([key, value]) => `${key}=${formatPerfValue(value)}`);
37
+ const suffix = formattedFields.length > 0 ? ` ${formattedFields.join(' ')}` : '';
38
+ console.info(`[perf][${scope}]${suffix}`);
39
+ }