veryfront 0.1.26 → 0.1.28

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 (124) hide show
  1. package/README.md +3 -11
  2. package/esm/cli/app/shell.d.ts.map +1 -1
  3. package/esm/cli/app/shell.js +9 -5
  4. package/esm/cli/commands/demo/demo.js +1 -1
  5. package/esm/cli/commands/init/catalog.d.ts.map +1 -1
  6. package/esm/cli/commands/init/catalog.js +13 -5
  7. package/esm/cli/commands/init/command-help.js +4 -4
  8. package/esm/cli/commands/init/types.d.ts +1 -1
  9. package/esm/cli/commands/init/types.d.ts.map +1 -1
  10. package/esm/cli/commands/serve/command.d.ts.map +1 -1
  11. package/esm/cli/commands/serve/command.js +0 -4
  12. package/esm/cli/commands/start/command.d.ts.map +1 -1
  13. package/esm/cli/commands/start/command.js +16 -9
  14. package/esm/cli/help/tips.js +6 -6
  15. package/esm/cli/mcp/remote-file-tools.js +1 -1
  16. package/esm/cli/mcp/tools/catalog-tools.d.ts +3 -3
  17. package/esm/cli/mcp/tools/catalog-tools.d.ts.map +1 -1
  18. package/esm/cli/mcp/tools/catalog-tools.js +21 -13
  19. package/esm/cli/mcp/tools/project-tools.js +1 -1
  20. package/esm/cli/templates/index.js +11 -11
  21. package/esm/cli/templates/manifest.d.ts +22 -15
  22. package/esm/cli/templates/manifest.js +24 -17
  23. package/esm/cli/templates/types.d.ts +1 -1
  24. package/esm/cli/templates/types.d.ts.map +1 -1
  25. package/esm/cli/utils/index.d.ts.map +1 -1
  26. package/esm/cli/utils/index.js +13 -1
  27. package/esm/deno.js +1 -1
  28. package/esm/src/html/html-shell-generator.d.ts.map +1 -1
  29. package/esm/src/html/html-shell-generator.js +2 -0
  30. package/esm/src/html/styles-builder/project-css-cache.d.ts +8 -1
  31. package/esm/src/html/styles-builder/project-css-cache.d.ts.map +1 -1
  32. package/esm/src/html/styles-builder/project-css-cache.js +13 -2
  33. package/esm/src/html/styles-builder/tailwind-compiler.d.ts +2 -0
  34. package/esm/src/html/styles-builder/tailwind-compiler.d.ts.map +1 -1
  35. package/esm/src/html/styles-builder/tailwind-compiler.js +52 -19
  36. package/esm/src/modules/react-loader/css-import-collector.d.ts +29 -0
  37. package/esm/src/modules/react-loader/css-import-collector.d.ts.map +1 -0
  38. package/esm/src/modules/react-loader/css-import-collector.js +41 -0
  39. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  40. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +6 -0
  41. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts.map +1 -1
  42. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.js +5 -0
  43. package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
  44. package/esm/src/platform/adapters/fs/factory.js +5 -1
  45. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +1 -0
  46. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  47. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +19 -5
  48. package/esm/src/platform/compat/process.d.ts.map +1 -1
  49. package/esm/src/platform/compat/process.js +20 -3
  50. package/esm/src/proxy/main.js +31 -12
  51. package/esm/src/proxy/token-manager.d.ts +2 -0
  52. package/esm/src/proxy/token-manager.d.ts.map +1 -1
  53. package/esm/src/proxy/token-manager.js +47 -8
  54. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +23 -0
  55. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -0
  56. package/esm/src/rendering/orchestrator/css-candidate-manifest.js +132 -0
  57. package/esm/src/rendering/orchestrator/html.d.ts +11 -1
  58. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  59. package/esm/src/rendering/orchestrator/html.js +103 -18
  60. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  61. package/esm/src/rendering/orchestrator/pipeline.js +14 -2
  62. package/esm/src/server/bootstrap.d.ts +2 -0
  63. package/esm/src/server/bootstrap.d.ts.map +1 -1
  64. package/esm/src/server/bootstrap.js +10 -0
  65. package/esm/src/server/handlers/preview/markdown-html-generator.d.ts.map +1 -1
  66. package/esm/src/server/handlers/preview/markdown-html-generator.js +11 -5
  67. package/esm/src/server/production-server.js +10 -2
  68. package/esm/src/studio/bridge-template.d.ts +2 -0
  69. package/esm/src/studio/bridge-template.d.ts.map +1 -1
  70. package/esm/src/studio/bridge-template.js +3390 -52
  71. package/esm/src/transforms/css-modules/naming.d.ts +33 -0
  72. package/esm/src/transforms/css-modules/naming.d.ts.map +1 -0
  73. package/esm/src/transforms/css-modules/naming.js +128 -0
  74. package/esm/src/transforms/esm/import-parser.d.ts +1 -0
  75. package/esm/src/transforms/esm/import-parser.d.ts.map +1 -1
  76. package/esm/src/transforms/esm/import-parser.js +16 -5
  77. package/esm/src/transforms/pipeline/index.d.ts.map +1 -1
  78. package/esm/src/transforms/pipeline/index.js +3 -1
  79. package/esm/src/transforms/pipeline/stages/index.d.ts +1 -0
  80. package/esm/src/transforms/pipeline/stages/index.d.ts.map +1 -1
  81. package/esm/src/transforms/pipeline/stages/index.js +1 -0
  82. package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts +18 -0
  83. package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts.map +1 -0
  84. package/esm/src/transforms/pipeline/stages/ssr-css-strip.js +168 -0
  85. package/package.json +1 -1
  86. package/src/cli/app/shell.ts +9 -5
  87. package/src/cli/commands/demo/demo.ts +1 -1
  88. package/src/cli/commands/init/catalog.ts +13 -5
  89. package/src/cli/commands/init/command-help.ts +4 -4
  90. package/src/cli/commands/init/types.ts +5 -5
  91. package/src/cli/commands/serve/command.ts +0 -5
  92. package/src/cli/commands/start/command.ts +15 -10
  93. package/src/cli/help/tips.ts +6 -6
  94. package/src/cli/mcp/remote-file-tools.ts +1 -1
  95. package/src/cli/mcp/tools/catalog-tools.ts +21 -13
  96. package/src/cli/mcp/tools/project-tools.ts +1 -1
  97. package/src/cli/templates/index.ts +11 -11
  98. package/src/cli/templates/manifest.js +24 -17
  99. package/src/cli/templates/types.ts +5 -5
  100. package/src/cli/utils/index.ts +12 -1
  101. package/src/deno.js +1 -1
  102. package/src/src/html/html-shell-generator.ts +2 -0
  103. package/src/src/html/styles-builder/project-css-cache.ts +24 -1
  104. package/src/src/html/styles-builder/tailwind-compiler.ts +67 -26
  105. package/src/src/modules/react-loader/css-import-collector.ts +50 -0
  106. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +7 -0
  107. package/src/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.ts +6 -0
  108. package/src/src/platform/adapters/fs/factory.ts +5 -1
  109. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +21 -5
  110. package/src/src/platform/compat/process.ts +28 -4
  111. package/src/src/proxy/main.ts +32 -12
  112. package/src/src/proxy/token-manager.ts +54 -8
  113. package/src/src/rendering/orchestrator/css-candidate-manifest.ts +176 -0
  114. package/src/src/rendering/orchestrator/html.ts +128 -16
  115. package/src/src/rendering/orchestrator/pipeline.ts +183 -165
  116. package/src/src/server/bootstrap.ts +16 -0
  117. package/src/src/server/handlers/preview/markdown-html-generator.ts +12 -5
  118. package/src/src/server/production-server.ts +12 -2
  119. package/src/src/studio/bridge-template.ts +3392 -52
  120. package/src/src/transforms/css-modules/naming.ts +152 -0
  121. package/src/src/transforms/esm/import-parser.ts +15 -5
  122. package/src/src/transforms/pipeline/index.ts +3 -0
  123. package/src/src/transforms/pipeline/stages/index.ts +1 -0
  124. package/src/src/transforms/pipeline/stages/ssr-css-strip.ts +201 -0
@@ -5,6 +5,8 @@ export function generateStudioBridgeScript(options) {
5
5
  const PROJECT_ID = ${JSON.stringify(options.projectId)};
6
6
  const PAGE_ID = ${JSON.stringify(options.pageId)};
7
7
  const PAGE_PATH = ${JSON.stringify(options.pagePath ?? options.pageId)};
8
+ const DEBUG_SKIP_INIT = ${options.debugSkipInit ? "true" : "false"};
9
+ const DEBUG_EXPOSE_INTERNALS = ${options.debugExposeInternals ? "true" : "false"};
8
10
 
9
11
  const DATA_VF_ID = 'data-vf-id';
10
12
  const DATA_VF_SELECTOR = 'data-vf-selector';
@@ -24,6 +26,100 @@ export function generateStudioBridgeScript(options) {
24
26
 
25
27
  let hoverOverlay = null;
26
28
  let selectionOverlay = null;
29
+ let markdownEditorRoot = null;
30
+ let markdownEditorSurface = null;
31
+ let markdownEditorTextarea = null;
32
+ let markdownEditButton = null;
33
+ let markdownFileId = null;
34
+ let markdownSyncTimer = null;
35
+ let markdownSelectionSyncTimer = null;
36
+ let markdownPersistStatus = null;
37
+ let markdownPresenceRoot = null;
38
+ let markdownSelectionsRoot = null;
39
+ let markdownSelectionOverlayRoot = null;
40
+ let markdownOverlaySelections = [];
41
+ let markdownSelectionOverlayRenderFrame = null;
42
+ let markdownSlashMenuRoot = null;
43
+ let markdownSlashMenuTimer = null;
44
+ let markdownSlashMenuContext = null;
45
+ let markdownSlashMenuCommands = [];
46
+ let markdownSlashMenuActiveIndex = 0;
47
+ let markdownInlineToolbarRoot = null;
48
+ let markdownInlineToolbarFrame = null;
49
+ let markdownBlockDragHandle = null;
50
+ let markdownBlockDropIndicator = null;
51
+ let markdownBlockDropLabel = null;
52
+ let markdownBlockDragGhost = null;
53
+ let markdownBlockDragSourceIndex = -1;
54
+ let markdownBlockDropSlotIndex = -1;
55
+ let markdownBlockHandleHoverIndex = -1;
56
+ let markdownBlockDragActive = false;
57
+ let markdownMdxBlocksRoot = null;
58
+ let markdownLexicalApi = null;
59
+ let markdownLexicalSetupPromise = null;
60
+ let markdownCurrentContent = '';
61
+ let markdownCurrentEditorContent = '';
62
+ let markdownLexicalRenderedContent = null;
63
+ let markdownApplyingRemoteUpdate = false;
64
+ let markdownFrontmatter = '';
65
+ let markdownRawBlocks = [];
66
+ let markdownRawBlockTokenPrefix = 'VF_RAW_BLOCK';
67
+ let markdownLatestMdxBlocks = [];
68
+ let markdownLatestMdxImportMap = {};
69
+ let markdownLatestPresenceUsers = [];
70
+ let markdownLatestSelections = [];
71
+ let markdownHasUnsavedChanges = false;
72
+
73
+ const MARKDOWN_SLASH_COMMANDS = [
74
+ {
75
+ id: 'heading-1',
76
+ label: 'Heading 1',
77
+ description: 'Create a top-level heading',
78
+ aliases: ['h1', 'heading', 'title']
79
+ },
80
+ {
81
+ id: 'heading-2',
82
+ label: 'Heading 2',
83
+ description: 'Create a second-level heading',
84
+ aliases: ['h2', 'heading2', 'subheading']
85
+ },
86
+ {
87
+ id: 'heading-3',
88
+ label: 'Heading 3',
89
+ description: 'Create a third-level heading',
90
+ aliases: ['h3', 'heading3']
91
+ },
92
+ {
93
+ id: 'bulleted-list',
94
+ label: 'Bulleted list',
95
+ description: 'Start a bullet list item',
96
+ aliases: ['list', 'bullet', 'ul']
97
+ },
98
+ {
99
+ id: 'numbered-list',
100
+ label: 'Numbered list',
101
+ description: 'Start a numbered list item',
102
+ aliases: ['olist', 'numbered', 'ol']
103
+ },
104
+ {
105
+ id: 'quote-block',
106
+ label: 'Quote',
107
+ description: 'Insert a block quote line',
108
+ aliases: ['quote', 'blockquote']
109
+ },
110
+ {
111
+ id: 'code-block',
112
+ label: 'Code block',
113
+ description: 'Insert a fenced code block',
114
+ aliases: ['code', 'fence', 'snippet']
115
+ },
116
+ {
117
+ id: 'image',
118
+ label: 'Image',
119
+ description: 'Insert markdown image syntax',
120
+ aliases: ['image', 'img', 'photo']
121
+ }
122
+ ];
27
123
 
28
124
  function debounce(fn, ms) {
29
125
  let timer;
@@ -74,8 +170,580 @@ export function generateStudioBridgeScript(options) {
74
170
  bottom: -22px;
75
171
  border-radius: 0 0 3px 3px;
76
172
  }
173
+
174
+ .vf-markdown-edit-button {
175
+ position: fixed;
176
+ right: 16px;
177
+ bottom: 16px;
178
+ z-index: 100001;
179
+ border: 1px solid rgba(0, 0, 0, 0.2);
180
+ border-radius: 8px;
181
+ background: #111827;
182
+ color: #ffffff;
183
+ font-size: 13px;
184
+ line-height: 1;
185
+ padding: 10px 12px;
186
+ cursor: pointer;
187
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
188
+ }
189
+
190
+ .vf-markdown-editor {
191
+ position: fixed;
192
+ inset: 0;
193
+ z-index: 100000;
194
+ background: var(--vf-markdown-editor-bg, #ffffff);
195
+ display: none;
196
+ }
197
+
198
+ .vf-markdown-editor__toolbar {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ align-items: center;
202
+ gap: 8px;
203
+ padding: 12px 16px;
204
+ border-bottom: 1px solid rgba(0, 0, 0, 0.12);
205
+ background: rgba(255, 255, 255, 0.94);
206
+ position: sticky;
207
+ top: 0;
208
+ }
209
+
210
+ .vf-markdown-editor__title {
211
+ font-size: 12px;
212
+ color: #374151;
213
+ font-weight: 600;
214
+ display: inline-flex;
215
+ flex-direction: column;
216
+ gap: 2px;
217
+ }
218
+
219
+ .vf-markdown-editor__title-main {
220
+ font-size: 12px;
221
+ font-weight: 700;
222
+ }
223
+
224
+ .vf-markdown-editor__title-hints {
225
+ font-size: 10px;
226
+ font-weight: 500;
227
+ color: #6b7280;
228
+ }
229
+
230
+ .vf-markdown-editor__actions {
231
+ display: inline-flex;
232
+ align-items: center;
233
+ gap: 8px;
234
+ }
235
+
236
+ .vf-markdown-editor__status {
237
+ font-size: 12px;
238
+ color: #6b7280;
239
+ }
240
+
241
+ .vf-markdown-editor__status[data-state='saving'] {
242
+ color: #b45309;
243
+ }
244
+
245
+ .vf-markdown-editor__status[data-state='saved'] {
246
+ color: #15803d;
247
+ }
248
+
249
+ .vf-markdown-editor__status[data-state='error'] {
250
+ color: #b91c1c;
251
+ }
252
+
253
+ .vf-markdown-editor__presence {
254
+ display: none;
255
+ align-items: center;
256
+ gap: 6px;
257
+ }
258
+
259
+ .vf-markdown-editor__presence-pill {
260
+ border: 1px solid rgba(0, 0, 0, 0.16);
261
+ border-left-width: 4px;
262
+ border-radius: 999px;
263
+ padding: 2px 8px;
264
+ font-size: 11px;
265
+ color: #111827;
266
+ background: #ffffff;
267
+ }
268
+
269
+ .vf-markdown-editor__presence-pill[data-current='true'] {
270
+ font-weight: 600;
271
+ }
272
+
273
+ .vf-markdown-editor__presence-pill[data-agent='true'] {
274
+ font-style: italic;
275
+ }
276
+
277
+ .vf-markdown-editor__selections {
278
+ display: none;
279
+ align-items: center;
280
+ gap: 6px;
281
+ }
282
+
283
+ .vf-markdown-editor__selection-pill {
284
+ border: 1px solid rgba(0, 0, 0, 0.16);
285
+ border-left-width: 4px;
286
+ border-radius: 999px;
287
+ padding: 2px 8px;
288
+ font-size: 11px;
289
+ color: #111827;
290
+ background: #ffffff;
291
+ }
292
+
293
+ .vf-markdown-editor__exit {
294
+ border: 1px solid rgba(0, 0, 0, 0.16);
295
+ border-radius: 6px;
296
+ background: #ffffff;
297
+ color: #111827;
298
+ font-size: 12px;
299
+ padding: 6px 10px;
300
+ cursor: pointer;
301
+ }
302
+
303
+ .vf-markdown-editor__history {
304
+ border: 1px solid rgba(0, 0, 0, 0.16);
305
+ border-radius: 6px;
306
+ background: #ffffff;
307
+ color: #111827;
308
+ font-size: 13px;
309
+ line-height: 1;
310
+ min-width: 28px;
311
+ height: 28px;
312
+ padding: 0 8px;
313
+ cursor: pointer;
314
+ }
315
+
316
+ .vf-markdown-editor__surface-wrap {
317
+ position: relative;
318
+ height: calc(100vh - 52px);
319
+ }
320
+
321
+ .vf-markdown-editor__surface {
322
+ width: 100%;
323
+ max-width: 980px;
324
+ margin: 0 auto;
325
+ height: 100%;
326
+ overflow: auto;
327
+ outline: none;
328
+ position: relative;
329
+ z-index: 1;
330
+ background: transparent;
331
+ padding: 32px 40px;
332
+ box-sizing: border-box;
333
+ }
334
+
335
+ .vf-markdown-editor__selection-overlay {
336
+ position: absolute;
337
+ inset: 0;
338
+ pointer-events: none;
339
+ z-index: 2;
340
+ display: none;
341
+ }
342
+
343
+ .vf-markdown-editor__selection-highlight {
344
+ position: absolute;
345
+ border-radius: 3px;
346
+ opacity: 0.26;
347
+ }
348
+
349
+ .vf-markdown-editor__selection-caret {
350
+ position: absolute;
351
+ width: 2px;
352
+ border-radius: 1px;
353
+ }
354
+
355
+ .vf-markdown-editor__selection-label {
356
+ position: absolute;
357
+ transform: translateY(-100%);
358
+ margin-top: -4px;
359
+ border-radius: 999px;
360
+ padding: 1px 7px;
361
+ font-size: 10px;
362
+ line-height: 1.4;
363
+ white-space: nowrap;
364
+ color: #ffffff;
365
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
366
+ }
367
+
368
+ .vf-markdown-editor__slash-menu {
369
+ position: fixed;
370
+ z-index: 100005;
371
+ min-width: 220px;
372
+ max-width: 300px;
373
+ border: 1px solid rgba(17, 24, 39, 0.16);
374
+ border-radius: 10px;
375
+ background: #ffffff;
376
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
377
+ padding: 6px;
378
+ display: none;
379
+ }
380
+
381
+ .vf-markdown-editor__slash-item {
382
+ display: block;
383
+ width: 100%;
384
+ border: 0;
385
+ border-radius: 8px;
386
+ background: transparent;
387
+ text-align: left;
388
+ padding: 8px 10px;
389
+ cursor: pointer;
390
+ }
391
+
392
+ .vf-markdown-editor__slash-item:hover,
393
+ .vf-markdown-editor__slash-item[data-active='true'] {
394
+ background: rgba(0, 129, 248, 0.12);
395
+ }
396
+
397
+ .vf-markdown-editor__slash-item-title {
398
+ display: block;
399
+ font-size: 12px;
400
+ font-weight: 600;
401
+ color: #111827;
402
+ line-height: 1.35;
403
+ }
404
+
405
+ .vf-markdown-editor__slash-item-desc {
406
+ display: block;
407
+ margin-top: 2px;
408
+ font-size: 11px;
409
+ color: #6b7280;
410
+ line-height: 1.35;
411
+ }
412
+
413
+ .vf-markdown-editor__inline-toolbar {
414
+ position: fixed;
415
+ z-index: 100006;
416
+ display: none;
417
+ align-items: center;
418
+ gap: 2px;
419
+ border: 1px solid rgba(17, 24, 39, 0.16);
420
+ border-radius: 8px;
421
+ background: #ffffff;
422
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16);
423
+ padding: 4px;
424
+ }
425
+
426
+ .vf-markdown-editor__inline-button {
427
+ border: 0;
428
+ border-radius: 6px;
429
+ background: transparent;
430
+ color: #111827;
431
+ font-size: 12px;
432
+ font-weight: 600;
433
+ line-height: 1;
434
+ min-width: 26px;
435
+ height: 24px;
436
+ padding: 0 7px;
437
+ cursor: pointer;
438
+ }
439
+
440
+ .vf-markdown-editor__inline-button:hover {
441
+ background: rgba(0, 129, 248, 0.12);
442
+ }
443
+
444
+ .vf-markdown-editor__block-handle {
445
+ position: fixed;
446
+ z-index: 100007;
447
+ display: none;
448
+ border: 1px solid rgba(17, 24, 39, 0.18);
449
+ border-radius: 6px;
450
+ background: #ffffff;
451
+ color: #374151;
452
+ font-size: 12px;
453
+ font-weight: 700;
454
+ line-height: 1;
455
+ width: 28px;
456
+ height: 28px;
457
+ padding: 0;
458
+ cursor: grab;
459
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
460
+ }
461
+
462
+ .vf-markdown-editor__block-handle:hover {
463
+ background: rgba(0, 129, 248, 0.12);
464
+ }
465
+
466
+ .vf-markdown-editor__block-handle[data-dragging='true'] {
467
+ cursor: grabbing;
468
+ }
469
+
470
+ .vf-markdown-editor__block-drop-indicator {
471
+ position: fixed;
472
+ z-index: 100006;
473
+ display: none;
474
+ height: 2px;
475
+ border-radius: 999px;
476
+ background: #0081f8;
477
+ box-shadow: 0 1px 6px rgba(0, 129, 248, 0.5);
478
+ }
479
+
480
+ .vf-markdown-editor__block-drop-label {
481
+ position: fixed;
482
+ z-index: 100007;
483
+ display: none;
484
+ border-radius: 999px;
485
+ border: 1px solid rgba(0, 129, 248, 0.24);
486
+ background: rgba(255, 255, 255, 0.96);
487
+ color: #0f172a;
488
+ font-size: 11px;
489
+ font-weight: 600;
490
+ line-height: 1.2;
491
+ padding: 3px 8px;
492
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
493
+ }
494
+
495
+ .vf-markdown-editor__block-drag-ghost {
496
+ position: fixed;
497
+ top: -9999px;
498
+ left: -9999px;
499
+ width: 260px;
500
+ border: 1px solid rgba(17, 24, 39, 0.22);
501
+ border-radius: 10px;
502
+ background: #ffffff;
503
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
504
+ padding: 8px 10px;
505
+ }
506
+
507
+ .vf-markdown-editor__block-drag-ghost-title {
508
+ display: block;
509
+ font-size: 11px;
510
+ font-weight: 700;
511
+ color: #1e293b;
512
+ }
513
+
514
+ .vf-markdown-editor__block-drag-ghost-text {
515
+ display: block;
516
+ margin-top: 4px;
517
+ font-size: 11px;
518
+ color: #475569;
519
+ line-height: 1.35;
520
+ }
521
+
522
+ .vf-markdown-editor__mdx-blocks {
523
+ display: none;
524
+ gap: 8px;
525
+ padding: 8px 16px;
526
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
527
+ background: rgba(245, 247, 250, 0.95);
528
+ overflow-x: auto;
529
+ }
530
+
531
+ .vf-markdown-editor__mdx-block {
532
+ display: inline-flex;
533
+ align-items: center;
534
+ gap: 8px;
535
+ border: 1px solid rgba(0, 0, 0, 0.16);
536
+ border-radius: 8px;
537
+ background: #ffffff;
538
+ padding: 6px 8px;
539
+ }
540
+
541
+ .vf-markdown-editor__mdx-block-label {
542
+ font-size: 11px;
543
+ color: #334155;
544
+ white-space: nowrap;
545
+ }
546
+
547
+ .vf-markdown-editor__mdx-note {
548
+ font-size: 10px;
549
+ color: #6b7280;
550
+ white-space: nowrap;
551
+ }
552
+
553
+ .vf-markdown-editor__mdx-open {
554
+ border: 1px solid rgba(0, 0, 0, 0.16);
555
+ border-radius: 6px;
556
+ background: #ffffff;
557
+ color: #0f172a;
558
+ font-size: 11px;
559
+ line-height: 1;
560
+ padding: 5px 7px;
561
+ cursor: pointer;
562
+ white-space: nowrap;
563
+ }
564
+
565
+ .vf-markdown-editor__surface [data-lexical-editor] {
566
+ outline: none;
567
+ }
568
+
569
+ .vf-markdown-editor__surface p:empty::before {
570
+ content: '';
571
+ display: inline-block;
572
+ }
573
+
574
+ .vf-markdown-editor__surface p {
575
+ min-height: 1.5em;
576
+ }
577
+
578
+ .vf-markdown-editor__textarea {
579
+ width: 100%;
580
+ height: calc(100vh - 52px);
581
+ border: 0;
582
+ outline: none;
583
+ resize: none;
584
+ display: none;
585
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
586
+ font-size: 14px;
587
+ line-height: 1.6;
588
+ color: #111827;
589
+ background: transparent;
590
+ padding: 16px;
591
+ box-sizing: border-box;
592
+ }
593
+
594
+ [data-theme='dark'] .vf-markdown-editor {
595
+ --vf-markdown-editor-bg: #0b1220;
596
+ }
597
+
598
+ [data-theme='dark'] .vf-markdown-editor__toolbar {
599
+ border-bottom-color: rgba(255, 255, 255, 0.18);
600
+ background: rgba(17, 24, 39, 0.92);
601
+ }
602
+
603
+ [data-theme='dark'] .vf-markdown-editor__title {
604
+ color: #d1d5db;
605
+ }
606
+
607
+ [data-theme='dark'] .vf-markdown-editor__title-hints {
608
+ color: #9ca3af;
609
+ }
610
+
611
+ [data-theme='dark'] .vf-markdown-editor__exit {
612
+ background: #111827;
613
+ border-color: rgba(255, 255, 255, 0.2);
614
+ color: #f9fafb;
615
+ }
616
+
617
+ [data-theme='dark'] .vf-markdown-editor__history {
618
+ background: #111827;
619
+ border-color: rgba(255, 255, 255, 0.2);
620
+ color: #f9fafb;
621
+ }
622
+
623
+ [data-theme='dark'] .vf-markdown-editor__status {
624
+ color: #9ca3af;
625
+ }
626
+
627
+ [data-theme='dark'] .vf-markdown-editor__status[data-state='saving'] {
628
+ color: #fbbf24;
629
+ }
630
+
631
+ [data-theme='dark'] .vf-markdown-editor__status[data-state='saved'] {
632
+ color: #4ade80;
633
+ }
634
+
635
+ [data-theme='dark'] .vf-markdown-editor__status[data-state='error'] {
636
+ color: #f87171;
637
+ }
638
+
639
+ [data-theme='dark'] .vf-markdown-editor__presence-pill {
640
+ border-color: rgba(255, 255, 255, 0.2);
641
+ color: #f9fafb;
642
+ background: #111827;
643
+ }
644
+
645
+ [data-theme='dark'] .vf-markdown-editor__selection-pill {
646
+ border-color: rgba(255, 255, 255, 0.2);
647
+ color: #f9fafb;
648
+ background: #111827;
649
+ }
650
+
651
+ [data-theme='dark'] .vf-markdown-editor__textarea {
652
+ color: #f9fafb;
653
+ }
654
+
655
+ [data-theme='dark'] .vf-markdown-editor__slash-menu {
656
+ border-color: rgba(255, 255, 255, 0.2);
657
+ background: #111827;
658
+ }
659
+
660
+ [data-theme='dark'] .vf-markdown-editor__slash-item:hover,
661
+ [data-theme='dark'] .vf-markdown-editor__slash-item[data-active='true'] {
662
+ background: rgba(59, 130, 246, 0.24);
663
+ }
664
+
665
+ [data-theme='dark'] .vf-markdown-editor__slash-item-title {
666
+ color: #f9fafb;
667
+ }
668
+
669
+ [data-theme='dark'] .vf-markdown-editor__slash-item-desc {
670
+ color: #9ca3af;
671
+ }
672
+
673
+ [data-theme='dark'] .vf-markdown-editor__inline-toolbar {
674
+ border-color: rgba(255, 255, 255, 0.2);
675
+ background: #111827;
676
+ }
677
+
678
+ [data-theme='dark'] .vf-markdown-editor__inline-button {
679
+ color: #f9fafb;
680
+ }
681
+
682
+ [data-theme='dark'] .vf-markdown-editor__inline-button:hover {
683
+ background: rgba(59, 130, 246, 0.24);
684
+ }
685
+
686
+ [data-theme='dark'] .vf-markdown-editor__block-handle {
687
+ border-color: rgba(255, 255, 255, 0.22);
688
+ background: #111827;
689
+ color: #d1d5db;
690
+ }
691
+
692
+ [data-theme='dark'] .vf-markdown-editor__block-handle:hover {
693
+ background: rgba(59, 130, 246, 0.24);
694
+ }
695
+
696
+ [data-theme='dark'] .vf-markdown-editor__block-drop-label {
697
+ border-color: rgba(59, 130, 246, 0.35);
698
+ background: rgba(17, 24, 39, 0.94);
699
+ color: #e5e7eb;
700
+ }
701
+
702
+ [data-theme='dark'] .vf-markdown-editor__block-drag-ghost {
703
+ border-color: rgba(255, 255, 255, 0.24);
704
+ background: #111827;
705
+ }
706
+
707
+ [data-theme='dark'] .vf-markdown-editor__block-drag-ghost-title {
708
+ color: #e5e7eb;
709
+ }
710
+
711
+ [data-theme='dark'] .vf-markdown-editor__block-drag-ghost-text {
712
+ color: #94a3b8;
713
+ }
714
+
715
+ [data-theme='dark'] .vf-markdown-editor__mdx-blocks {
716
+ border-bottom-color: rgba(255, 255, 255, 0.12);
717
+ background: rgba(2, 6, 23, 0.7);
718
+ }
719
+
720
+ [data-theme='dark'] .vf-markdown-editor__mdx-block {
721
+ border-color: rgba(255, 255, 255, 0.22);
722
+ background: #111827;
723
+ }
724
+
725
+ [data-theme='dark'] .vf-markdown-editor__mdx-block-label {
726
+ color: #cbd5e1;
727
+ }
728
+
729
+ [data-theme='dark'] .vf-markdown-editor__mdx-note {
730
+ color: #94a3b8;
731
+ }
732
+
733
+ [data-theme='dark'] .vf-markdown-editor__mdx-open {
734
+ border-color: rgba(255, 255, 255, 0.22);
735
+ background: #0b1220;
736
+ color: #e5e7eb;
737
+ }
77
738
  \`;
78
- document.head.appendChild(style);
739
+ try {
740
+ document.head.appendChild(style);
741
+ if (!style.sheet) {
742
+ console.warn('[StudioBridge] Inline style injection may be blocked by CSP (style-src).');
743
+ }
744
+ } catch (error) {
745
+ console.warn('[StudioBridge] Failed to inject bridge styles. This may be caused by CSP style-src restrictions.', error);
746
+ }
79
747
  }
80
748
 
81
749
  function createOverlay(type) {
@@ -147,13 +815,18 @@ export function generateStudioBridgeScript(options) {
147
815
  }
148
816
 
149
817
  function isFromStudio(event) {
150
- const origin = event.origin || '';
151
- return (
152
- origin.includes('veryfront.org') ||
153
- origin.includes('veryfront.com') ||
154
- origin.includes('veryfront.dev') ||
155
- origin.includes('localhost')
156
- );
818
+ try {
819
+ const url = new URL(event.origin || '');
820
+ const host = url.hostname;
821
+ return (
822
+ host === 'localhost' ||
823
+ host.endsWith('.veryfront.org') || host === 'veryfront.org' ||
824
+ host.endsWith('.veryfront.com') || host === 'veryfront.com' ||
825
+ host.endsWith('.veryfront.dev') || host === 'veryfront.dev'
826
+ );
827
+ } catch (e) {
828
+ return false;
829
+ }
157
830
  }
158
831
 
159
832
  const originalConsole = {};
@@ -475,59 +1148,2673 @@ export function generateStudioBridgeScript(options) {
475
1148
  document.documentElement.classList.add(mode);
476
1149
  }
477
1150
 
478
- let html2canvasLoaded = false;
479
- let html2canvasPromise = null;
480
-
481
- function loadHtml2Canvas() {
482
- if (html2canvasLoaded) return Promise.resolve();
483
- if (html2canvasPromise) return html2canvasPromise;
484
-
485
- html2canvasPromise = new Promise((resolve, reject) => {
486
- const script = document.createElement('script');
487
- script.src = 'https://cdn.jsdelivr.net/npm/html2canvas-pro@2.0.0/dist/html2canvas-pro.min.js';
488
- script.onload = () => {
489
- html2canvasLoaded = true;
490
- resolve();
491
- };
492
- script.onerror = reject;
493
- document.head.appendChild(script);
494
- });
1151
+ function isMarkdownPage() {
1152
+ if (typeof PAGE_PATH !== 'string') {
1153
+ return false;
1154
+ }
1155
+ const lowerPath = PAGE_PATH.toLowerCase();
1156
+ return lowerPath.endsWith('.md') || lowerPath.endsWith('.mdx');
1157
+ }
495
1158
 
496
- return html2canvasPromise;
1159
+ function isMdxPage() {
1160
+ return typeof PAGE_PATH === 'string' && PAGE_PATH.toLowerCase().endsWith('.mdx');
497
1161
  }
498
1162
 
499
- async function captureScreenshot(options) {
500
- const { scrollTo, fullPage, quality = 0.8 } = options || {};
501
- const originalScrollY = window.scrollY;
1163
+ function openFilePathInStudio(filePath, lineNumber, symbolName) {
1164
+ if (typeof filePath !== 'string' || !filePath) {
1165
+ return;
1166
+ }
1167
+ const safeLine = Number.isFinite(lineNumber) ? Math.max(1, Math.trunc(lineNumber)) : 1;
1168
+ const payload = {
1169
+ action: 'openFile',
1170
+ filePath: filePath,
1171
+ lineNumber: safeLine,
1172
+ columnNumber: 1
1173
+ };
1174
+ if (typeof symbolName === 'string' && symbolName.trim()) {
1175
+ payload.symbolName = symbolName.trim();
1176
+ }
1177
+ postToStudio(payload);
1178
+ }
502
1179
 
503
- try {
504
- await loadHtml2Canvas();
1180
+ function openMarkdownSourceInStudio(lineNumber) {
1181
+ openFilePathInStudio(PAGE_PATH, lineNumber);
1182
+ }
505
1183
 
506
- if (typeof scrollTo === 'number') {
507
- window.scrollTo(0, scrollTo);
508
- await new Promise(r => setTimeout(r, 150));
1184
+ function normalizePathSegments(segments) {
1185
+ const stack = [];
1186
+ for (const segment of segments) {
1187
+ if (!segment || segment === '.') {
1188
+ continue;
1189
+ }
1190
+ if (segment === '..') {
1191
+ if (stack.length > 0) {
1192
+ stack.pop();
1193
+ }
1194
+ continue;
509
1195
  }
1196
+ stack.push(segment);
1197
+ }
1198
+ return stack;
1199
+ }
510
1200
 
511
- const canvasOptions = {
512
- useCORS: true,
513
- logging: false,
514
- scale: window.devicePixelRatio || 1
515
- };
1201
+ function resolveImportPathForPage(importPath) {
1202
+ const sourcePath = typeof importPath === 'string' ? importPath.trim() : '';
1203
+ if (!sourcePath) {
1204
+ return '';
1205
+ }
516
1206
 
517
- if (fullPage) {
518
- canvasOptions.height = document.documentElement.scrollHeight;
519
- canvasOptions.windowHeight = document.documentElement.scrollHeight;
520
- canvasOptions.y = 0;
521
- window.scrollTo(0, 0);
522
- await new Promise(r => setTimeout(r, 100));
1207
+ if (sourcePath.startsWith('@/') || sourcePath.startsWith('~/')) {
1208
+ return sourcePath.slice(2);
1209
+ }
1210
+
1211
+ if (sourcePath.startsWith('/')) {
1212
+ let normalizedPath = sourcePath;
1213
+ while (normalizedPath.startsWith('/')) {
1214
+ normalizedPath = normalizedPath.slice(1);
523
1215
  }
1216
+ return normalizedPath;
1217
+ }
524
1218
 
525
- const html2canvasFn = window.html2canvas.default || window.html2canvas;
526
- const canvas = await html2canvasFn(document.body, canvasOptions);
1219
+ if (!PAGE_PATH || !sourcePath.startsWith('.')) {
1220
+ return sourcePath;
1221
+ }
527
1222
 
528
- if (!canvas || canvas.width === 0 || canvas.height === 0) {
529
- console.error('[bridge] html2canvas produced empty canvas:', canvas?.width, 'x', canvas?.height);
530
- window.scrollTo(0, originalScrollY);
1223
+ const baseParts = String(PAGE_PATH).split('/');
1224
+ baseParts.pop();
1225
+ const resolved = normalizePathSegments(baseParts.concat(sourcePath.split('/')));
1226
+ return resolved.join('/');
1227
+ }
1228
+
1229
+ function isLikelyProjectImportPath(importPath) {
1230
+ if (typeof importPath !== 'string') {
1231
+ return false;
1232
+ }
1233
+ const value = importPath.trim();
1234
+ if (!value) {
1235
+ return false;
1236
+ }
1237
+ return (
1238
+ value.startsWith('.') ||
1239
+ value.startsWith('/') ||
1240
+ value.startsWith('@/') ||
1241
+ value.startsWith('~/')
1242
+ );
1243
+ }
1244
+
1245
+ function guessStudioFilePath(filePath) {
1246
+ const sourcePath = typeof filePath === 'string' ? filePath.trim() : '';
1247
+ if (!sourcePath) {
1248
+ return '';
1249
+ }
1250
+
1251
+ const hasKnownExtension = sourcePath.match(/\\.(tsx?|jsx?|mdx?|json|css|scss|sass|less)$/i);
1252
+ if (hasKnownExtension) {
1253
+ return sourcePath;
1254
+ }
1255
+
1256
+ if (sourcePath.endsWith('/')) {
1257
+ return sourcePath + 'index.tsx';
1258
+ }
1259
+
1260
+ return sourcePath + '.tsx';
1261
+ }
1262
+
1263
+ function parseMdxImportMap(content) {
1264
+ const source = typeof content === 'string' ? content : '';
1265
+ const importMap = {};
1266
+ if (!source) {
1267
+ return importMap;
1268
+ }
1269
+
1270
+ const stripImportComments = function(specifierText) {
1271
+ return String(specifierText || '')
1272
+ .replace(/\\/\\*[\\s\\S]*?\\*\\//g, ' ')
1273
+ .replace(/\\/\\/[^\\n\\r]*/g, ' ');
1274
+ };
1275
+
1276
+ const normalizeImportSpecifier = function(specifierText) {
1277
+ return stripImportComments(specifierText)
1278
+ .replace(/\\s+/g, ' ')
1279
+ .trim();
1280
+ };
1281
+
1282
+ const setImportEntry = function(localName, resolvedPath, symbolName, importKind) {
1283
+ const key = typeof localName === 'string' ? localName.trim() : '';
1284
+ const filePath = typeof resolvedPath === 'string' ? resolvedPath.trim() : '';
1285
+ if (!key || !filePath) {
1286
+ return;
1287
+ }
1288
+ importMap[key] = {
1289
+ filePath: filePath,
1290
+ symbolName: typeof symbolName === 'string' ? symbolName.trim() : '',
1291
+ importKind: typeof importKind === 'string' ? importKind : 'unknown'
1292
+ };
1293
+ };
1294
+
1295
+ const mapNamedImports = function(namedSpecifier, resolvedPath) {
1296
+ const text = String(namedSpecifier || '').trim();
1297
+ if (!text.startsWith('{') || !text.endsWith('}')) {
1298
+ return;
1299
+ }
1300
+ const named = text.slice(1, -1).split(',');
1301
+ for (const entry of named) {
1302
+ const part = entry.trim();
1303
+ if (!part) {
1304
+ continue;
1305
+ }
1306
+ const normalizedPart = normalizeImportSpecifier(part).trim();
1307
+ if (!normalizedPart || /^type\\s+/.test(normalizedPart)) {
1308
+ continue;
1309
+ }
1310
+ const aliasMatch = normalizedPart.match(/^([A-Za-z_$][\\w$]*)\\s+as\\s+([A-Za-z_$][\\w$]*)$/);
1311
+ const sourceName = aliasMatch ? aliasMatch[1] : normalizedPart;
1312
+ const localName = aliasMatch ? aliasMatch[2] : normalizedPart;
1313
+ if (localName) {
1314
+ const isDefaultAlias = sourceName === 'default';
1315
+ setImportEntry(localName, resolvedPath, isDefaultAlias ? '' : sourceName, isDefaultAlias ? 'default' : 'named');
1316
+ }
1317
+ }
1318
+ };
1319
+
1320
+ const importPattern = /^import\\s+([\\s\\S]*?)\\s+from\\s+['\"]([^'\"]+)['\"]\\s*;?/gm;
1321
+ let match = importPattern.exec(source);
1322
+ while (match) {
1323
+ const specifier = normalizeImportSpecifier(match[1] || '');
1324
+ if (!specifier) {
1325
+ match = importPattern.exec(source);
1326
+ continue;
1327
+ }
1328
+ const typeOnlySpecifier = specifier.startsWith('type ');
1329
+ const normalizedSpecifier = typeOnlySpecifier ? specifier.slice(5).trim() : specifier;
1330
+ if (typeOnlySpecifier) {
1331
+ match = importPattern.exec(source);
1332
+ continue;
1333
+ }
1334
+ const rawImportPath = String(match[2] || '').trim();
1335
+ if (!isLikelyProjectImportPath(rawImportPath)) {
1336
+ match = importPattern.exec(source);
1337
+ continue;
1338
+ }
1339
+ const resolvedPath = guessStudioFilePath(resolveImportPathForPage(rawImportPath));
1340
+ if (!resolvedPath) {
1341
+ match = importPattern.exec(source);
1342
+ continue;
1343
+ }
1344
+
1345
+ if (normalizedSpecifier.startsWith('{') && normalizedSpecifier.endsWith('}')) {
1346
+ mapNamedImports(normalizedSpecifier, resolvedPath);
1347
+ } else if (normalizedSpecifier.startsWith('* as ')) {
1348
+ const namespaceName = normalizedSpecifier.slice(5).trim();
1349
+ if (namespaceName) {
1350
+ setImportEntry(namespaceName, resolvedPath, '', 'namespace');
1351
+ }
1352
+ } else {
1353
+ const commaIndex = normalizedSpecifier.indexOf(',');
1354
+ if (commaIndex >= 0) {
1355
+ const defaultPart = normalizedSpecifier.slice(0, commaIndex).trim();
1356
+ const restPart = normalizedSpecifier.slice(commaIndex + 1).trim();
1357
+ const normalizedDefaultPart = defaultPart.trim();
1358
+ if (normalizedDefaultPart && !/^type\\s+/.test(normalizedDefaultPart)) {
1359
+ setImportEntry(normalizedDefaultPart, resolvedPath, '', 'default');
1360
+ }
1361
+ if (restPart.startsWith('{')) {
1362
+ mapNamedImports(restPart, resolvedPath);
1363
+ } else if (restPart.startsWith('* as ')) {
1364
+ const namespaceName = restPart.slice(5).trim();
1365
+ if (namespaceName) {
1366
+ setImportEntry(namespaceName, resolvedPath, '', 'namespace');
1367
+ }
1368
+ }
1369
+ } else {
1370
+ const defaultPart = normalizedSpecifier.trim();
1371
+ const normalizedDefaultPart = defaultPart.trim();
1372
+ if (normalizedDefaultPart && !/^type\\s+/.test(normalizedDefaultPart)) {
1373
+ setImportEntry(normalizedDefaultPart, resolvedPath, '', 'default');
1374
+ }
1375
+ }
1376
+ }
1377
+
1378
+ match = importPattern.exec(source);
1379
+ }
1380
+
1381
+ return importMap;
1382
+ }
1383
+
1384
+ function postMarkdownEditorReady() {
1385
+ if (!markdownFileId) {
1386
+ return;
1387
+ }
1388
+ postToStudio({
1389
+ action: 'markdownEditorReady',
1390
+ fileId: markdownFileId,
1391
+ filePath: PAGE_PATH
1392
+ });
1393
+ }
1394
+
1395
+ function scheduleMarkdownSync(content) {
1396
+ if (!markdownFileId) {
1397
+ return;
1398
+ }
1399
+ if (markdownSyncTimer) {
1400
+ clearTimeout(markdownSyncTimer);
1401
+ }
1402
+ markdownSyncTimer = setTimeout(function() {
1403
+ postToStudio({
1404
+ action: 'markdownContentChange',
1405
+ fileId: markdownFileId,
1406
+ filePath: PAGE_PATH,
1407
+ content: content
1408
+ });
1409
+ }, 120);
1410
+ }
1411
+
1412
+ function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
1413
+ if (!root || !targetNode) {
1414
+ return 0;
1415
+ }
1416
+
1417
+ if (!root.contains(targetNode)) {
1418
+ return 0;
1419
+ }
1420
+
1421
+ try {
1422
+ const range = document.createRange();
1423
+ range.selectNodeContents(root);
1424
+ range.setEnd(targetNode, targetOffset);
1425
+ return range.toString().length;
1426
+ } catch {
1427
+ return 0;
1428
+ }
1429
+ }
1430
+
1431
+ function getMarkdownEditorSelection() {
1432
+ if (markdownLexicalApi && markdownEditorSurface) {
1433
+ const selection = window.getSelection();
1434
+ if (!selection || selection.rangeCount === 0) {
1435
+ return null;
1436
+ }
1437
+
1438
+ const range = selection.getRangeAt(0);
1439
+ if (
1440
+ !markdownEditorSurface.contains(range.startContainer) ||
1441
+ !markdownEditorSurface.contains(range.endContainer)
1442
+ ) {
1443
+ return null;
1444
+ }
1445
+
1446
+ const start = getTextOffsetWithinRoot(
1447
+ markdownEditorSurface,
1448
+ range.startContainer,
1449
+ range.startOffset
1450
+ );
1451
+ const end = getTextOffsetWithinRoot(
1452
+ markdownEditorSurface,
1453
+ range.endContainer,
1454
+ range.endOffset
1455
+ );
1456
+ return {
1457
+ start: Math.max(0, Math.min(start, end)),
1458
+ end: Math.max(0, Math.max(start, end))
1459
+ };
1460
+ }
1461
+
1462
+ if (markdownEditorTextarea) {
1463
+ const start = typeof markdownEditorTextarea.selectionStart === 'number'
1464
+ ? markdownEditorTextarea.selectionStart
1465
+ : 0;
1466
+ const end = typeof markdownEditorTextarea.selectionEnd === 'number'
1467
+ ? markdownEditorTextarea.selectionEnd
1468
+ : start;
1469
+
1470
+ return {
1471
+ start: Math.max(0, Math.min(start, end)),
1472
+ end: Math.max(0, Math.max(start, end))
1473
+ };
1474
+ }
1475
+
1476
+ return null;
1477
+ }
1478
+
1479
+ function getMarkdownRawBlockLength(index) {
1480
+ const rawBlock = markdownRawBlocks[index];
1481
+ if (typeof rawBlock !== 'string') {
1482
+ return 0;
1483
+ }
1484
+ return rawBlock.length;
1485
+ }
1486
+
1487
+ function escapeRegexText(value) {
1488
+ const text = String(value || '');
1489
+ let escaped = '';
1490
+ for (let i = 0; i < text.length; i += 1) {
1491
+ const char = text[i];
1492
+ if ('\\\\^$.*+?()[]{}|'.indexOf(char) >= 0) {
1493
+ escaped += '\\\\' + char;
1494
+ } else {
1495
+ escaped += char;
1496
+ }
1497
+ }
1498
+ return escaped;
1499
+ }
1500
+
1501
+ function getMarkdownRawBlockTokenPattern() {
1502
+ const prefix = typeof markdownRawBlockTokenPrefix === 'string' && markdownRawBlockTokenPrefix
1503
+ ? markdownRawBlockTokenPrefix
1504
+ : 'VF_RAW_BLOCK';
1505
+ const escapedPrefix = escapeRegexText(prefix);
1506
+ return new RegExp('\\\\[\\\\[' + escapedPrefix + '_(\\\\d+)\\\\]\\\\]', 'g');
1507
+ }
1508
+
1509
+ function editorOffsetToBodyOffset(editorOffset, bias) {
1510
+ const editorContent = typeof markdownCurrentEditorContent === 'string'
1511
+ ? markdownCurrentEditorContent
1512
+ : '';
1513
+ const maxOffset = editorContent.length;
1514
+ const safeOffset = Math.max(0, Math.min(maxOffset, Math.trunc(editorOffset || 0)));
1515
+ const tokenPattern = getMarkdownRawBlockTokenPattern();
1516
+ let diffBefore = 0;
1517
+ let match = tokenPattern.exec(editorContent);
1518
+
1519
+ while (match) {
1520
+ const token = match[0];
1521
+ const tokenStartEditor = match.index;
1522
+ const tokenEndEditor = tokenStartEditor + token.length;
1523
+ const rawLength = getMarkdownRawBlockLength(Number(match[1]));
1524
+ const tokenDelta = rawLength - token.length;
1525
+ const tokenStartBody = tokenStartEditor + diffBefore;
1526
+
1527
+ if (safeOffset >= tokenEndEditor) {
1528
+ diffBefore += tokenDelta;
1529
+ match = tokenPattern.exec(editorContent);
1530
+ continue;
1531
+ }
1532
+
1533
+ if (safeOffset > tokenStartEditor) {
1534
+ if (bias === 'end') {
1535
+ return tokenStartBody + rawLength;
1536
+ }
1537
+ return tokenStartBody;
1538
+ }
1539
+ break;
1540
+ }
1541
+
1542
+ return safeOffset + diffBefore;
1543
+ }
1544
+
1545
+ function bodyOffsetToEditorOffset(bodyOffset, bias) {
1546
+ const editorContent = typeof markdownCurrentEditorContent === 'string'
1547
+ ? markdownCurrentEditorContent
1548
+ : '';
1549
+ const safeBodyOffset = Math.max(0, Math.trunc(bodyOffset || 0));
1550
+ const tokenPattern = getMarkdownRawBlockTokenPattern();
1551
+ let diffBefore = 0;
1552
+ let match = tokenPattern.exec(editorContent);
1553
+
1554
+ while (match) {
1555
+ const token = match[0];
1556
+ const tokenStartEditor = match.index;
1557
+ const tokenEndEditor = tokenStartEditor + token.length;
1558
+ const rawLength = getMarkdownRawBlockLength(Number(match[1]));
1559
+ const tokenStartBody = tokenStartEditor + diffBefore;
1560
+ const tokenEndBody = tokenStartBody + rawLength;
1561
+
1562
+ if (safeBodyOffset > tokenEndBody) {
1563
+ diffBefore += rawLength - token.length;
1564
+ match = tokenPattern.exec(editorContent);
1565
+ continue;
1566
+ }
1567
+
1568
+ if (safeBodyOffset >= tokenStartBody && safeBodyOffset <= tokenEndBody) {
1569
+ if (bias === 'end') {
1570
+ return tokenEndEditor;
1571
+ }
1572
+ return tokenStartEditor;
1573
+ }
1574
+
1575
+ const mappedOffset = safeBodyOffset - diffBefore;
1576
+ return Math.max(0, Math.min(editorContent.length, mappedOffset));
1577
+ }
1578
+
1579
+ const mappedOffset = safeBodyOffset - diffBefore;
1580
+ return Math.max(0, Math.min(editorContent.length, mappedOffset));
1581
+ }
1582
+
1583
+ function editorOffsetToSourceOffset(editorOffset, bias) {
1584
+ const frontmatterLength = typeof markdownFrontmatter === 'string' ? markdownFrontmatter.length : 0;
1585
+ const bodyOffset = editorOffsetToBodyOffset(editorOffset, bias);
1586
+ return Math.max(0, frontmatterLength + bodyOffset);
1587
+ }
1588
+
1589
+ function sourceSelectionToEditorRange(start, end) {
1590
+ const frontmatterLength = typeof markdownFrontmatter === 'string' ? markdownFrontmatter.length : 0;
1591
+ const safeStart = Math.max(0, Math.trunc(start || 0));
1592
+ const safeEnd = Math.max(0, Math.trunc(end || 0));
1593
+ const sourceStart = Math.min(safeStart, safeEnd);
1594
+ const sourceEnd = Math.max(safeStart, safeEnd);
1595
+
1596
+ if (sourceEnd <= frontmatterLength) {
1597
+ return null;
1598
+ }
1599
+
1600
+ const bodyStart = Math.max(0, sourceStart - frontmatterLength);
1601
+ const bodyEnd = Math.max(0, sourceEnd - frontmatterLength);
1602
+ const editorStart = bodyOffsetToEditorOffset(bodyStart, 'start');
1603
+ const editorEnd = bodyOffsetToEditorOffset(bodyEnd, 'end');
1604
+
1605
+ return {
1606
+ start: Math.max(0, Math.min(editorStart, editorEnd)),
1607
+ end: Math.max(0, Math.max(editorStart, editorEnd))
1608
+ };
1609
+ }
1610
+
1611
+ function setMarkdownEditorSelection(start, end) {
1612
+ const safeStart = Math.max(0, Math.trunc(start || 0));
1613
+ const endValue = typeof end === 'number' ? end : safeStart;
1614
+ const safeEnd = Math.max(0, Math.trunc(endValue));
1615
+
1616
+ if (markdownLexicalApi && markdownEditorSurface) {
1617
+ const selection = window.getSelection();
1618
+ if (!selection) {
1619
+ return;
1620
+ }
1621
+
1622
+ const anchor = resolveMarkdownTextPoint(markdownEditorSurface, safeStart);
1623
+ const focus = resolveMarkdownTextPoint(markdownEditorSurface, safeEnd);
1624
+ try {
1625
+ const range = document.createRange();
1626
+ range.setStart(anchor.node, anchor.offset);
1627
+ range.setEnd(focus.node, focus.offset);
1628
+ selection.removeAllRanges();
1629
+ selection.addRange(range);
1630
+ } catch {
1631
+ // Ignore when point resolution races with Lexical DOM updates.
1632
+ }
1633
+ return;
1634
+ }
1635
+
1636
+ if (markdownEditorTextarea) {
1637
+ const max = markdownEditorTextarea.value.length;
1638
+ markdownEditorTextarea.setSelectionRange(Math.min(safeStart, max), Math.min(safeEnd, max));
1639
+ }
1640
+ }
1641
+
1642
+ function hideMarkdownSlashMenu() {
1643
+ markdownSlashMenuContext = null;
1644
+ markdownSlashMenuCommands = [];
1645
+ markdownSlashMenuActiveIndex = 0;
1646
+ if (!markdownSlashMenuRoot) {
1647
+ return;
1648
+ }
1649
+ markdownSlashMenuRoot.style.display = 'none';
1650
+ markdownSlashMenuRoot.textContent = '';
1651
+ }
1652
+
1653
+ function getMarkdownSlashCommandInsert(id, indent) {
1654
+ const prefix = typeof indent === 'string' ? indent : '';
1655
+ if (id === 'heading-1') {
1656
+ const text = prefix + '# ';
1657
+ return { text: text, caretOffset: text.length };
1658
+ }
1659
+ if (id === 'heading-2') {
1660
+ const text = prefix + '## ';
1661
+ return { text: text, caretOffset: text.length };
1662
+ }
1663
+ if (id === 'heading-3') {
1664
+ const text = prefix + '### ';
1665
+ return { text: text, caretOffset: text.length };
1666
+ }
1667
+ if (id === 'bulleted-list') {
1668
+ const text = prefix + '- ';
1669
+ return { text: text, caretOffset: text.length };
1670
+ }
1671
+ if (id === 'numbered-list') {
1672
+ const text = prefix + '1. ';
1673
+ return { text: text, caretOffset: text.length };
1674
+ }
1675
+ if (id === 'quote-block') {
1676
+ const text = prefix + '> ';
1677
+ return { text: text, caretOffset: text.length };
1678
+ }
1679
+ if (id === 'code-block') {
1680
+ const fence = String.fromCharCode(96, 96, 96);
1681
+ const text = prefix + fence + '\\n' + prefix + '\\n' + prefix + fence;
1682
+ return {
1683
+ text: text,
1684
+ caretOffset: (prefix + fence + '\\n' + prefix).length
1685
+ };
1686
+ }
1687
+ if (id === 'image') {
1688
+ const text = prefix + '![alt text](https://)';
1689
+ return {
1690
+ text: text,
1691
+ caretOffset: (prefix + '![alt text](').length
1692
+ };
1693
+ }
1694
+ return null;
1695
+ }
1696
+
1697
+ function applyMarkdownSlashCommand(index) {
1698
+ if (!markdownSlashMenuContext || markdownSlashMenuCommands.length === 0) {
1699
+ return false;
1700
+ }
1701
+
1702
+ const command = markdownSlashMenuCommands[index];
1703
+ if (!command) {
1704
+ return false;
1705
+ }
1706
+
1707
+ const insert = getMarkdownSlashCommandInsert(command.id, markdownSlashMenuContext.indent);
1708
+ if (!insert) {
1709
+ return false;
1710
+ }
1711
+
1712
+ const editorContent = typeof markdownCurrentEditorContent === 'string'
1713
+ ? markdownCurrentEditorContent
1714
+ : '';
1715
+ const before = editorContent.slice(0, markdownSlashMenuContext.lineStart);
1716
+ const after = editorContent.slice(markdownSlashMenuContext.caret);
1717
+ const nextEditorContent = before + insert.text + after;
1718
+ const nextCaret = before.length + insert.caretOffset;
1719
+ const nextFullContent = composeMarkdownContent(restoreRawBlocksFromEditor(nextEditorContent));
1720
+ const hasChanged = nextFullContent !== markdownCurrentContent;
1721
+
1722
+ applyMarkdownContent(nextFullContent);
1723
+ if (hasChanged) {
1724
+ markdownHasUnsavedChanges = true;
1725
+ scheduleMarkdownSync(nextFullContent);
1726
+ }
1727
+
1728
+ setTimeout(function() {
1729
+ focusMarkdownEditor();
1730
+ setMarkdownEditorSelection(nextCaret, nextCaret);
1731
+ scheduleMarkdownSelectionSync();
1732
+ scheduleMarkdownSelectionOverlayRender();
1733
+ scheduleMarkdownSlashMenuUpdate();
1734
+ }, 0);
1735
+
1736
+ hideMarkdownSlashMenu();
1737
+ return true;
1738
+ }
1739
+
1740
+ function renderMarkdownSlashMenu() {
1741
+ if (!markdownSlashMenuRoot || !markdownSlashMenuContext || markdownSlashMenuCommands.length === 0) {
1742
+ hideMarkdownSlashMenu();
1743
+ return;
1744
+ }
1745
+
1746
+ markdownSlashMenuRoot.textContent = '';
1747
+
1748
+ const maxLeft = Math.max(8, window.innerWidth - 312);
1749
+ const maxTop = Math.max(8, window.innerHeight - 220);
1750
+ const left = Math.max(8, Math.min(maxLeft, markdownSlashMenuContext.anchorLeft));
1751
+ const top = Math.max(8, Math.min(maxTop, markdownSlashMenuContext.anchorTop));
1752
+ markdownSlashMenuRoot.style.left = left + 'px';
1753
+ markdownSlashMenuRoot.style.top = top + 'px';
1754
+
1755
+ markdownSlashMenuCommands.forEach(function(command, index) {
1756
+ const item = document.createElement('button');
1757
+ item.type = 'button';
1758
+ item.className = 'vf-markdown-editor__slash-item';
1759
+ item.setAttribute('data-active', index === markdownSlashMenuActiveIndex ? 'true' : 'false');
1760
+ item.setAttribute(DATA_VF_IGNORE, 'true');
1761
+ item.addEventListener('mousedown', function(event) {
1762
+ event.preventDefault();
1763
+ });
1764
+ item.addEventListener('click', function(event) {
1765
+ event.preventDefault();
1766
+ markdownSlashMenuActiveIndex = index;
1767
+ applyMarkdownSlashCommand(markdownSlashMenuActiveIndex);
1768
+ });
1769
+
1770
+ const title = document.createElement('span');
1771
+ title.className = 'vf-markdown-editor__slash-item-title';
1772
+ title.textContent = command.label;
1773
+
1774
+ const description = document.createElement('span');
1775
+ description.className = 'vf-markdown-editor__slash-item-desc';
1776
+ description.textContent = command.description;
1777
+
1778
+ item.appendChild(title);
1779
+ item.appendChild(description);
1780
+ markdownSlashMenuRoot.appendChild(item);
1781
+ });
1782
+
1783
+ markdownSlashMenuRoot.style.display = 'block';
1784
+ }
1785
+
1786
+ function updateMarkdownSlashMenu() {
1787
+ if (
1788
+ !markdownEditorRoot ||
1789
+ markdownEditorRoot.style.display !== 'block' ||
1790
+ !markdownLexicalApi ||
1791
+ !markdownEditorSurface ||
1792
+ markdownEditorSurface.style.display === 'none'
1793
+ ) {
1794
+ hideMarkdownSlashMenu();
1795
+ return;
1796
+ }
1797
+
1798
+ const selection = getMarkdownEditorSelection();
1799
+ if (!selection || selection.start !== selection.end) {
1800
+ hideMarkdownSlashMenu();
1801
+ return;
1802
+ }
1803
+
1804
+ const caret = selection.start;
1805
+ const editorContent = typeof markdownCurrentEditorContent === 'string'
1806
+ ? markdownCurrentEditorContent
1807
+ : '';
1808
+ const lineStart = editorContent.lastIndexOf('\\n', Math.max(0, caret - 1)) + 1;
1809
+ const line = editorContent.slice(lineStart, caret);
1810
+ const match = line.match(/^(\\s*)\\/([a-z0-9-]*)$/i);
1811
+ if (!match) {
1812
+ hideMarkdownSlashMenu();
1813
+ return;
1814
+ }
1815
+
1816
+ const query = (match[2] || '').toLowerCase();
1817
+ const commands = MARKDOWN_SLASH_COMMANDS.filter(function(command) {
1818
+ if (!query) {
1819
+ return true;
1820
+ }
1821
+ if (command.label.toLowerCase().includes(query)) {
1822
+ return true;
1823
+ }
1824
+ return command.aliases.some(function(alias) {
1825
+ return alias.startsWith(query);
1826
+ });
1827
+ });
1828
+
1829
+ if (commands.length === 0) {
1830
+ hideMarkdownSlashMenu();
1831
+ return;
1832
+ }
1833
+
1834
+ const domSelection = window.getSelection();
1835
+ if (!domSelection || domSelection.rangeCount === 0) {
1836
+ hideMarkdownSlashMenu();
1837
+ return;
1838
+ }
1839
+ const caretRect = domSelection.getRangeAt(0).getBoundingClientRect();
1840
+ const anchorLeft = caretRect.left;
1841
+ const anchorTop = caretRect.bottom + 8;
1842
+
1843
+ markdownSlashMenuCommands = commands.slice(0, 8);
1844
+ markdownSlashMenuActiveIndex = Math.max(0, Math.min(markdownSlashMenuActiveIndex, markdownSlashMenuCommands.length - 1));
1845
+ markdownSlashMenuContext = {
1846
+ lineStart: lineStart,
1847
+ caret: caret,
1848
+ indent: match[1] || '',
1849
+ query: query,
1850
+ anchorLeft: anchorLeft,
1851
+ anchorTop: anchorTop
1852
+ };
1853
+ renderMarkdownSlashMenu();
1854
+ }
1855
+
1856
+ function scheduleMarkdownSlashMenuUpdate() {
1857
+ if (markdownSlashMenuTimer) {
1858
+ clearTimeout(markdownSlashMenuTimer);
1859
+ }
1860
+ markdownSlashMenuTimer = setTimeout(function() {
1861
+ markdownSlashMenuTimer = null;
1862
+ updateMarkdownSlashMenu();
1863
+ }, 0);
1864
+ }
1865
+
1866
+ function handleMarkdownSlashMenuKeydown(event) {
1867
+ if (!markdownSlashMenuRoot || markdownSlashMenuRoot.style.display !== 'block' || markdownSlashMenuCommands.length === 0) {
1868
+ return false;
1869
+ }
1870
+
1871
+ if (event.key === 'ArrowDown') {
1872
+ event.preventDefault();
1873
+ markdownSlashMenuActiveIndex = (markdownSlashMenuActiveIndex + 1) % markdownSlashMenuCommands.length;
1874
+ renderMarkdownSlashMenu();
1875
+ return true;
1876
+ }
1877
+
1878
+ if (event.key === 'ArrowUp') {
1879
+ event.preventDefault();
1880
+ markdownSlashMenuActiveIndex = (markdownSlashMenuActiveIndex - 1 + markdownSlashMenuCommands.length) % markdownSlashMenuCommands.length;
1881
+ renderMarkdownSlashMenu();
1882
+ return true;
1883
+ }
1884
+
1885
+ if (event.key === 'Enter' || event.key === 'Tab') {
1886
+ event.preventDefault();
1887
+ return applyMarkdownSlashCommand(markdownSlashMenuActiveIndex);
1888
+ }
1889
+
1890
+ if (event.key === 'Escape') {
1891
+ event.preventDefault();
1892
+ hideMarkdownSlashMenu();
1893
+ return true;
1894
+ }
1895
+
1896
+ return false;
1897
+ }
1898
+
1899
+ function hideMarkdownInlineToolbar() {
1900
+ if (!markdownInlineToolbarRoot) {
1901
+ return;
1902
+ }
1903
+ markdownInlineToolbarRoot.style.display = 'none';
1904
+ }
1905
+
1906
+ function toggleMarkdownInlineFormat(format) {
1907
+ if (!markdownLexicalApi || !markdownLexicalApi.editor || !markdownLexicalApi.lexicalModule) {
1908
+ return;
1909
+ }
1910
+ if (typeof format !== 'string' || !format) {
1911
+ return;
1912
+ }
1913
+
1914
+ markdownLexicalApi.editor.focus();
1915
+ markdownLexicalApi.editor.dispatchCommand(markdownLexicalApi.lexicalModule.FORMAT_TEXT_COMMAND, format);
1916
+ scheduleMarkdownInlineToolbarUpdate();
1917
+ }
1918
+
1919
+ function updateMarkdownInlineToolbar() {
1920
+ if (
1921
+ !markdownInlineToolbarRoot ||
1922
+ !markdownEditorRoot ||
1923
+ markdownEditorRoot.style.display !== 'block' ||
1924
+ !markdownLexicalApi ||
1925
+ !markdownEditorSurface ||
1926
+ markdownEditorSurface.style.display === 'none'
1927
+ ) {
1928
+ hideMarkdownInlineToolbar();
1929
+ return;
1930
+ }
1931
+
1932
+ const selection = window.getSelection();
1933
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
1934
+ hideMarkdownInlineToolbar();
1935
+ return;
1936
+ }
1937
+
1938
+ const range = selection.getRangeAt(0);
1939
+ if (
1940
+ !markdownEditorSurface.contains(range.startContainer) ||
1941
+ !markdownEditorSurface.contains(range.endContainer)
1942
+ ) {
1943
+ hideMarkdownInlineToolbar();
1944
+ return;
1945
+ }
1946
+
1947
+ const rect = range.getBoundingClientRect();
1948
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
1949
+ hideMarkdownInlineToolbar();
1950
+ return;
1951
+ }
1952
+
1953
+ const left = Math.max(8, Math.min(window.innerWidth - 180, rect.left + rect.width / 2 - 66));
1954
+ const top = Math.max(8, Math.min(window.innerHeight - 56, rect.top - 44));
1955
+ markdownInlineToolbarRoot.style.left = left + 'px';
1956
+ markdownInlineToolbarRoot.style.top = top + 'px';
1957
+ markdownInlineToolbarRoot.style.display = 'inline-flex';
1958
+ }
1959
+
1960
+ function scheduleMarkdownInlineToolbarUpdate() {
1961
+ if (markdownInlineToolbarFrame) {
1962
+ cancelAnimationFrame(markdownInlineToolbarFrame);
1963
+ }
1964
+
1965
+ markdownInlineToolbarFrame = requestAnimationFrame(function() {
1966
+ markdownInlineToolbarFrame = null;
1967
+ updateMarkdownInlineToolbar();
1968
+ });
1969
+ }
1970
+
1971
+ function getMarkdownTopLevelBlocks() {
1972
+ if (!markdownEditorSurface) {
1973
+ return [];
1974
+ }
1975
+
1976
+ return Array.from(markdownEditorSurface.children).filter(function(node) {
1977
+ return node && node.nodeType === Node.ELEMENT_NODE;
1978
+ });
1979
+ }
1980
+
1981
+ function hideMarkdownBlockDragHandle() {
1982
+ markdownBlockHandleHoverIndex = -1;
1983
+ if (!markdownBlockDragHandle) {
1984
+ return;
1985
+ }
1986
+ markdownBlockDragHandle.style.display = 'none';
1987
+ markdownBlockDragHandle.removeAttribute('data-block-index');
1988
+ }
1989
+
1990
+ function hideMarkdownBlockDropIndicator() {
1991
+ markdownBlockDropSlotIndex = -1;
1992
+ if (markdownBlockDropIndicator) {
1993
+ markdownBlockDropIndicator.style.display = 'none';
1994
+ }
1995
+ if (markdownBlockDropLabel) {
1996
+ markdownBlockDropLabel.style.display = 'none';
1997
+ markdownBlockDropLabel.textContent = '';
1998
+ }
1999
+ }
2000
+
2001
+ function hideMarkdownBlockDragUi() {
2002
+ markdownBlockDragActive = false;
2003
+ markdownBlockDragSourceIndex = -1;
2004
+ if (markdownBlockDragHandle) {
2005
+ markdownBlockDragHandle.setAttribute('data-dragging', 'false');
2006
+ }
2007
+ removeMarkdownDragGhost();
2008
+ hideMarkdownBlockDropIndicator();
2009
+ hideMarkdownBlockDragHandle();
2010
+ }
2011
+
2012
+ function getMarkdownBlockElementFromNode(node) {
2013
+ if (!markdownEditorSurface || !node) {
2014
+ return null;
2015
+ }
2016
+
2017
+ let current = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
2018
+ while (current && current.parentElement !== markdownEditorSurface) {
2019
+ current = current.parentElement;
2020
+ }
2021
+
2022
+ if (!current || current.parentElement !== markdownEditorSurface) {
2023
+ return null;
2024
+ }
2025
+ return current;
2026
+ }
2027
+
2028
+ function getMarkdownBlockTypeInfo(block) {
2029
+ if (!block || !block.tagName) {
2030
+ return { label: 'block', color: '#0081f8' };
2031
+ }
2032
+
2033
+ const tag = block.tagName.toLowerCase();
2034
+ if (tag === 'h1') {
2035
+ return { label: 'heading 1', color: '#7c3aed' };
2036
+ }
2037
+ if (tag === 'h2') {
2038
+ return { label: 'heading 2', color: '#7c3aed' };
2039
+ }
2040
+ if (tag === 'h3') {
2041
+ return { label: 'heading 3', color: '#7c3aed' };
2042
+ }
2043
+ if (tag === 'ul' || tag === 'ol') {
2044
+ return { label: 'list', color: '#0d9488' };
2045
+ }
2046
+ if (tag === 'blockquote') {
2047
+ return { label: 'quote', color: '#2563eb' };
2048
+ }
2049
+ if (tag === 'pre') {
2050
+ return { label: 'code block', color: '#ea580c' };
2051
+ }
2052
+ if (tag === 'img' || tag === 'figure') {
2053
+ return { label: 'image', color: '#db2777' };
2054
+ }
2055
+ if (tag === 'p') {
2056
+ return { label: 'paragraph', color: '#16a34a' };
2057
+ }
2058
+ return { label: tag, color: '#0081f8' };
2059
+ }
2060
+
2061
+ function getMarkdownBlockPreviewText(block) {
2062
+ if (!block) {
2063
+ return '';
2064
+ }
2065
+ const text = String(block.textContent || '').replace(new RegExp('\\s+', 'g'), ' ').trim();
2066
+ if (!text) {
2067
+ return 'Empty block';
2068
+ }
2069
+ if (text.length > 84) {
2070
+ return text.slice(0, 84) + '...';
2071
+ }
2072
+ return text;
2073
+ }
2074
+
2075
+ function removeMarkdownDragGhost() {
2076
+ if (!markdownBlockDragGhost) {
2077
+ return;
2078
+ }
2079
+ markdownBlockDragGhost.remove();
2080
+ markdownBlockDragGhost = null;
2081
+ }
2082
+
2083
+ function createMarkdownDragGhost(block) {
2084
+ const typeInfo = getMarkdownBlockTypeInfo(block);
2085
+ const ghost = document.createElement('div');
2086
+ ghost.className = 'vf-markdown-editor__block-drag-ghost';
2087
+ ghost.setAttribute(DATA_VF_IGNORE, 'true');
2088
+
2089
+ const title = document.createElement('span');
2090
+ title.className = 'vf-markdown-editor__block-drag-ghost-title';
2091
+ title.setAttribute(DATA_VF_IGNORE, 'true');
2092
+ title.textContent = 'Moving ' + typeInfo.label;
2093
+
2094
+ const text = document.createElement('span');
2095
+ text.className = 'vf-markdown-editor__block-drag-ghost-text';
2096
+ text.setAttribute(DATA_VF_IGNORE, 'true');
2097
+ text.textContent = getMarkdownBlockPreviewText(block);
2098
+
2099
+ ghost.appendChild(title);
2100
+ ghost.appendChild(text);
2101
+ return ghost;
2102
+ }
2103
+
2104
+ function getLineNumberForOffset(text, offset) {
2105
+ const source = typeof text === 'string' ? text : '';
2106
+ const maxOffset = Math.max(0, Math.min(source.length, Math.trunc(offset || 0)));
2107
+ let line = 1;
2108
+ for (let i = 0; i < maxOffset; i += 1) {
2109
+ if (source.charCodeAt(i) === 10) {
2110
+ line += 1;
2111
+ }
2112
+ }
2113
+ return line;
2114
+ }
2115
+
2116
+ function getMdxComponentName(blockText) {
2117
+ const source = typeof blockText === 'string' ? blockText : '';
2118
+ const fence = String.fromCharCode(96, 96, 96);
2119
+ const componentMatch = source.match(/<\\s*([A-Z][\\w.]*)/);
2120
+ if (componentMatch && componentMatch[1]) {
2121
+ return componentMatch[1];
2122
+ }
2123
+ if (source.trim().startsWith(fence + 'tsx')) {
2124
+ return 'tsx block';
2125
+ }
2126
+ if (source.trim().startsWith(fence + 'jsx')) {
2127
+ return 'jsx block';
2128
+ }
2129
+ return 'component block';
2130
+ }
2131
+
2132
+ function getMdxBlockOpenUiState(block) {
2133
+ const hasResolvedTarget = !!(block && typeof block.filePath === 'string' && block.filePath.trim());
2134
+ return {
2135
+ hasResolvedTarget: hasResolvedTarget,
2136
+ buttonLabel: hasResolvedTarget ? 'Edit in Studio' : 'Open MDX source',
2137
+ showUnresolvedNote: !hasResolvedTarget
2138
+ };
2139
+ }
2140
+
2141
+ function setMarkdownMdxBlocks(blocks) {
2142
+ markdownLatestMdxBlocks = Array.isArray(blocks) ? blocks : [];
2143
+ if (!markdownMdxBlocksRoot) {
2144
+ return;
2145
+ }
2146
+
2147
+ markdownMdxBlocksRoot.textContent = '';
2148
+ if (!isMdxPage() || markdownLatestMdxBlocks.length === 0) {
2149
+ markdownMdxBlocksRoot.style.display = 'none';
2150
+ return;
2151
+ }
2152
+
2153
+ markdownMdxBlocksRoot.style.display = 'flex';
2154
+ for (const block of markdownLatestMdxBlocks.slice(0, 8)) {
2155
+ if (!block || typeof block.label !== 'string') {
2156
+ continue;
2157
+ }
2158
+
2159
+ const item = document.createElement('div');
2160
+ item.className = 'vf-markdown-editor__mdx-block';
2161
+ item.setAttribute(DATA_VF_IGNORE, 'true');
2162
+
2163
+ const label = document.createElement('div');
2164
+ label.className = 'vf-markdown-editor__mdx-block-label';
2165
+ label.setAttribute(DATA_VF_IGNORE, 'true');
2166
+ const safeLine = Number.isFinite(block.lineNumber) ? Math.max(1, Math.trunc(block.lineNumber)) : 1;
2167
+ label.textContent = block.label + ' (line ' + String(safeLine) + ')';
2168
+ const openUiState = getMdxBlockOpenUiState(block);
2169
+
2170
+ const openButton = document.createElement('button');
2171
+ openButton.type = 'button';
2172
+ openButton.className = 'vf-markdown-editor__mdx-open';
2173
+ openButton.setAttribute(DATA_VF_IGNORE, 'true');
2174
+ openButton.textContent = openUiState.buttonLabel;
2175
+ if (openUiState.showUnresolvedNote) {
2176
+ openButton.title = 'Component import could not be resolved. Opening current MDX source.';
2177
+ }
2178
+ openButton.addEventListener('click', function() {
2179
+ const targetFile = typeof block.filePath === 'string' && block.filePath ? block.filePath : PAGE_PATH;
2180
+ const targetLine = targetFile === PAGE_PATH ? safeLine : 1;
2181
+ const targetSymbol = targetFile === PAGE_PATH
2182
+ ? ''
2183
+ : (typeof block.symbolName === 'string' ? block.symbolName : '');
2184
+ openFilePathInStudio(targetFile, targetLine, targetSymbol);
2185
+ });
2186
+
2187
+ item.appendChild(label);
2188
+ if (openUiState.showUnresolvedNote) {
2189
+ const fallbackNote = document.createElement('span');
2190
+ fallbackNote.className = 'vf-markdown-editor__mdx-note';
2191
+ fallbackNote.setAttribute(DATA_VF_IGNORE, 'true');
2192
+ fallbackNote.textContent = 'Unresolved import';
2193
+ item.appendChild(fallbackNote);
2194
+ }
2195
+ item.appendChild(openButton);
2196
+ markdownMdxBlocksRoot.appendChild(item);
2197
+ }
2198
+
2199
+ if (markdownMdxBlocksRoot.childNodes.length === 0) {
2200
+ markdownMdxBlocksRoot.style.display = 'none';
2201
+ }
2202
+ }
2203
+
2204
+ function getMarkdownBlockHoverIndexFromPointer(targetNode, clientX, clientY) {
2205
+ const blocks = getMarkdownTopLevelBlocks();
2206
+ if (blocks.length === 0) {
2207
+ return -1;
2208
+ }
2209
+
2210
+ const directBlock = getMarkdownBlockElementFromNode(targetNode);
2211
+ const directIndex = blocks.indexOf(directBlock);
2212
+ if (directIndex >= 0) {
2213
+ return directIndex;
2214
+ }
2215
+
2216
+ if (!markdownEditorSurface) {
2217
+ return -1;
2218
+ }
2219
+ const surfaceRect = markdownEditorSurface.getBoundingClientRect();
2220
+ const leftBoundary = surfaceRect.left - 44;
2221
+ const rightBoundary = surfaceRect.left + Math.min(96, surfaceRect.width * 0.35);
2222
+ if (clientX < leftBoundary || clientX > rightBoundary) {
2223
+ return -1;
2224
+ }
2225
+
2226
+ for (let i = 0; i < blocks.length; i += 1) {
2227
+ const rect = blocks[i].getBoundingClientRect();
2228
+ if (clientY >= rect.top - 4 && clientY <= rect.bottom + 4) {
2229
+ return i;
2230
+ }
2231
+ }
2232
+
2233
+ const firstRect = blocks[0].getBoundingClientRect();
2234
+ const lastRect = blocks[blocks.length - 1].getBoundingClientRect();
2235
+ if (clientY < firstRect.top) {
2236
+ return 0;
2237
+ }
2238
+ if (clientY > lastRect.bottom) {
2239
+ return blocks.length - 1;
2240
+ }
2241
+ return -1;
2242
+ }
2243
+
2244
+ function positionMarkdownBlockDragHandle(block, index) {
2245
+ if (!markdownBlockDragHandle || !block || !markdownEditorRoot || markdownEditorRoot.style.display !== 'block') {
2246
+ hideMarkdownBlockDragHandle();
2247
+ return;
2248
+ }
2249
+
2250
+ const rect = block.getBoundingClientRect();
2251
+ const surfaceRect = markdownEditorSurface ? markdownEditorSurface.getBoundingClientRect() : null;
2252
+ if (!surfaceRect || rect.width <= 0 || rect.height <= 0) {
2253
+ hideMarkdownBlockDragHandle();
2254
+ return;
2255
+ }
2256
+
2257
+ // Ignore blocks outside the visible scroll viewport.
2258
+ if (rect.bottom < surfaceRect.top || rect.top > surfaceRect.bottom) {
2259
+ hideMarkdownBlockDragHandle();
2260
+ return;
2261
+ }
2262
+
2263
+ const left = Math.max(6, rect.left - 36);
2264
+ const top = Math.max(6, rect.top + 1);
2265
+ markdownBlockDragHandle.style.left = left + 'px';
2266
+ markdownBlockDragHandle.style.top = top + 'px';
2267
+ markdownBlockDragHandle.style.display = 'block';
2268
+ markdownBlockDragHandle.setAttribute('data-block-index', String(index));
2269
+ markdownBlockHandleHoverIndex = index;
2270
+ }
2271
+
2272
+ function refreshMarkdownBlockDragHandlePosition() {
2273
+ if (markdownBlockDragActive || markdownBlockHandleHoverIndex < 0) {
2274
+ return;
2275
+ }
2276
+ const blocks = getMarkdownTopLevelBlocks();
2277
+ const block = blocks[markdownBlockHandleHoverIndex];
2278
+ if (!block) {
2279
+ hideMarkdownBlockDragHandle();
2280
+ return;
2281
+ }
2282
+ positionMarkdownBlockDragHandle(block, markdownBlockHandleHoverIndex);
2283
+ }
2284
+
2285
+ function getMarkdownDropSlotIndexFromPointer(clientY) {
2286
+ const blocks = getMarkdownTopLevelBlocks();
2287
+ if (blocks.length === 0) {
2288
+ return -1;
2289
+ }
2290
+
2291
+ for (let i = 0; i < blocks.length; i += 1) {
2292
+ const rect = blocks[i].getBoundingClientRect();
2293
+ const midpoint = rect.top + rect.height / 2;
2294
+ if (clientY < midpoint) {
2295
+ return i;
2296
+ }
2297
+ }
2298
+ return blocks.length;
2299
+ }
2300
+
2301
+ function autoScrollMarkdownSurfaceDuringDrag(clientY) {
2302
+ if (!markdownEditorSurface) {
2303
+ return;
2304
+ }
2305
+
2306
+ const rect = markdownEditorSurface.getBoundingClientRect();
2307
+ const threshold = 42;
2308
+ const maxStep = 20;
2309
+ let delta = 0;
2310
+
2311
+ if (clientY < rect.top + threshold) {
2312
+ const distance = rect.top + threshold - clientY;
2313
+ delta = -Math.min(maxStep, Math.max(2, Math.floor(distance / 3)));
2314
+ } else if (clientY > rect.bottom - threshold) {
2315
+ const distance = clientY - (rect.bottom - threshold);
2316
+ delta = Math.min(maxStep, Math.max(2, Math.floor(distance / 3)));
2317
+ }
2318
+
2319
+ if (delta !== 0) {
2320
+ markdownEditorSurface.scrollTop += delta;
2321
+ refreshMarkdownBlockDragHandlePosition();
2322
+ }
2323
+ }
2324
+
2325
+ function showMarkdownBlockDropIndicator(slotIndex) {
2326
+ if (!markdownBlockDropIndicator || !markdownEditorSurface) {
2327
+ return;
2328
+ }
2329
+
2330
+ const blocks = getMarkdownTopLevelBlocks();
2331
+ if (blocks.length === 0) {
2332
+ hideMarkdownBlockDropIndicator();
2333
+ return;
2334
+ }
2335
+
2336
+ const safeSlot = Math.max(0, Math.min(blocks.length, Math.trunc(slotIndex || 0)));
2337
+ const surfaceRect = markdownEditorSurface.getBoundingClientRect();
2338
+ let top = surfaceRect.top + 6;
2339
+
2340
+ if (safeSlot >= blocks.length) {
2341
+ const lastRect = blocks[blocks.length - 1].getBoundingClientRect();
2342
+ top = lastRect.bottom + 1;
2343
+ } else {
2344
+ const rect = blocks[safeSlot].getBoundingClientRect();
2345
+ top = rect.top - 1;
2346
+ }
2347
+
2348
+ markdownBlockDropIndicator.style.left = Math.max(8, surfaceRect.left + 8) + 'px';
2349
+ markdownBlockDropIndicator.style.top = Math.max(8, top) + 'px';
2350
+ markdownBlockDropIndicator.style.width = Math.max(40, surfaceRect.width - 16) + 'px';
2351
+ markdownBlockDropIndicator.style.display = 'block';
2352
+ markdownBlockDropSlotIndex = safeSlot;
2353
+
2354
+ const dropType = safeSlot >= blocks.length
2355
+ ? { label: 'end of document', color: '#0284c7' }
2356
+ : getMarkdownBlockTypeInfo(blocks[safeSlot]);
2357
+ markdownBlockDropIndicator.style.background = dropType.color;
2358
+ markdownBlockDropIndicator.style.boxShadow = '0 1px 6px ' + dropType.color;
2359
+
2360
+ if (markdownBlockDropLabel) {
2361
+ markdownBlockDropLabel.textContent = safeSlot >= blocks.length
2362
+ ? 'Drop at end'
2363
+ : 'Drop before ' + dropType.label;
2364
+ markdownBlockDropLabel.style.left = Math.max(8, surfaceRect.left + 8) + 'px';
2365
+ markdownBlockDropLabel.style.top = Math.max(8, top - 26) + 'px';
2366
+ markdownBlockDropLabel.style.borderColor = dropType.color;
2367
+ markdownBlockDropLabel.style.display = 'block';
2368
+ }
2369
+ }
2370
+
2371
+ function moveMarkdownLexicalBlock(sourceIndex, targetSlotIndex) {
2372
+ if (!markdownLexicalApi || !markdownLexicalApi.editor || !markdownLexicalApi.lexicalModule) {
2373
+ return false;
2374
+ }
2375
+
2376
+ const source = Math.trunc(sourceIndex);
2377
+ const targetSlot = Math.trunc(targetSlotIndex);
2378
+ if (!Number.isInteger(source) || !Number.isInteger(targetSlot)) {
2379
+ return false;
2380
+ }
2381
+
2382
+ let didMove = false;
2383
+ markdownLexicalApi.editor.update(function() {
2384
+ const root = markdownLexicalApi.lexicalModule.$getRoot();
2385
+ const children = root.getChildren();
2386
+ const maxSlot = children.length;
2387
+ if (source < 0 || source >= maxSlot || targetSlot < 0 || targetSlot > maxSlot) {
2388
+ return;
2389
+ }
2390
+
2391
+ let adjustedSlot = targetSlot;
2392
+ if (source < adjustedSlot) {
2393
+ adjustedSlot -= 1;
2394
+ }
2395
+ if (adjustedSlot === source) {
2396
+ return;
2397
+ }
2398
+
2399
+ const node = children[source];
2400
+ node.remove();
2401
+ const afterRemoval = root.getChildren();
2402
+ if (adjustedSlot >= afterRemoval.length) {
2403
+ root.append(node);
2404
+ } else {
2405
+ afterRemoval[adjustedSlot].insertBefore(node);
2406
+ }
2407
+ didMove = true;
2408
+ });
2409
+
2410
+ if (didMove) {
2411
+ scheduleMarkdownSelectionSync();
2412
+ scheduleMarkdownSelectionOverlayRender();
2413
+ scheduleMarkdownSlashMenuUpdate();
2414
+ scheduleMarkdownInlineToolbarUpdate();
2415
+ }
2416
+ return didMove;
2417
+ }
2418
+
2419
+ function getMarkdownCurrentBlockIndexFromSelection() {
2420
+ if (!markdownEditorSurface) {
2421
+ return -1;
2422
+ }
2423
+ const selection = window.getSelection();
2424
+ if (!selection || selection.rangeCount === 0) {
2425
+ return -1;
2426
+ }
2427
+
2428
+ const range = selection.getRangeAt(0);
2429
+ if (!markdownEditorSurface.contains(range.startContainer)) {
2430
+ return -1;
2431
+ }
2432
+
2433
+ const block = getMarkdownBlockElementFromNode(range.startContainer);
2434
+ if (!block) {
2435
+ return -1;
2436
+ }
2437
+
2438
+ const blocks = getMarkdownTopLevelBlocks();
2439
+ return blocks.indexOf(block);
2440
+ }
2441
+
2442
+ function moveMarkdownCurrentBlockByDelta(delta) {
2443
+ const blocks = getMarkdownTopLevelBlocks();
2444
+ if (blocks.length <= 1) {
2445
+ return false;
2446
+ }
2447
+
2448
+ const index = getMarkdownCurrentBlockIndexFromSelection();
2449
+ if (index < 0) {
2450
+ return false;
2451
+ }
2452
+
2453
+ const step = Math.sign(delta);
2454
+ if (step === 0) {
2455
+ return false;
2456
+ }
2457
+
2458
+ let targetSlot = index;
2459
+ if (step < 0) {
2460
+ targetSlot = Math.max(0, index - 1);
2461
+ } else {
2462
+ targetSlot = Math.min(blocks.length, index + 2);
2463
+ }
2464
+
2465
+ const moved = moveMarkdownLexicalBlock(index, targetSlot);
2466
+ if (moved) {
2467
+ setTimeout(function() {
2468
+ const nextBlocks = getMarkdownTopLevelBlocks();
2469
+ const nextIndex = Math.max(0, Math.min(nextBlocks.length - 1, index + step));
2470
+ const nextBlock = nextBlocks[nextIndex];
2471
+ if (nextBlock) {
2472
+ positionMarkdownBlockDragHandle(nextBlock, nextIndex);
2473
+ }
2474
+ }, 0);
2475
+ }
2476
+ return moved;
2477
+ }
2478
+
2479
+ function scheduleMarkdownSelectionSync() {
2480
+ if (!markdownFileId) {
2481
+ return;
2482
+ }
2483
+
2484
+ if (markdownSelectionSyncTimer) {
2485
+ clearTimeout(markdownSelectionSyncTimer);
2486
+ }
2487
+
2488
+ markdownSelectionSyncTimer = setTimeout(function() {
2489
+ const selection = getMarkdownEditorSelection();
2490
+ if (!selection) {
2491
+ return;
2492
+ }
2493
+
2494
+ const start = editorOffsetToSourceOffset(selection.start, 'start');
2495
+ const end = editorOffsetToSourceOffset(selection.end, 'end');
2496
+
2497
+ postToStudio({
2498
+ action: 'markdownSelectionChange',
2499
+ fileId: markdownFileId,
2500
+ filePath: PAGE_PATH,
2501
+ start: start,
2502
+ end: end
2503
+ });
2504
+ }, 80);
2505
+ }
2506
+
2507
+ function clearMarkdownSelectionSync() {
2508
+ if (!markdownFileId) {
2509
+ return;
2510
+ }
2511
+ postToStudio({
2512
+ action: 'markdownSelectionChange',
2513
+ fileId: markdownFileId,
2514
+ filePath: PAGE_PATH,
2515
+ start: -1,
2516
+ end: -1
2517
+ });
2518
+ }
2519
+
2520
+ function clearMarkdownSelectionOverlay() {
2521
+ if (!markdownSelectionOverlayRoot) {
2522
+ return;
2523
+ }
2524
+ markdownSelectionOverlayRoot.textContent = '';
2525
+ markdownSelectionOverlayRoot.style.display = 'none';
2526
+ }
2527
+
2528
+ function resolveMarkdownTextPoint(root, rawOffset) {
2529
+ const offset = Math.max(0, Math.trunc(rawOffset || 0));
2530
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2531
+ let remaining = offset;
2532
+ let lastTextNode = null;
2533
+ let node = walker.nextNode();
2534
+
2535
+ while (node) {
2536
+ lastTextNode = node;
2537
+ const textLength = node.textContent ? node.textContent.length : 0;
2538
+ if (remaining <= textLength) {
2539
+ return { node: node, offset: remaining };
2540
+ }
2541
+ remaining -= textLength;
2542
+ node = walker.nextNode();
2543
+ }
2544
+
2545
+ if (lastTextNode) {
2546
+ const textLength = lastTextNode.textContent ? lastTextNode.textContent.length : 0;
2547
+ return { node: lastTextNode, offset: textLength };
2548
+ }
2549
+
2550
+ return {
2551
+ node: root,
2552
+ offset: offset > 0 ? root.childNodes.length : 0
2553
+ };
2554
+ }
2555
+
2556
+ function createMarkdownEditorRange(start, end) {
2557
+ if (!markdownEditorSurface) {
2558
+ return null;
2559
+ }
2560
+
2561
+ const safeStart = Math.max(0, Math.min(start, end));
2562
+ const safeEnd = Math.max(0, Math.max(start, end));
2563
+ const startPoint = resolveMarkdownTextPoint(markdownEditorSurface, safeStart);
2564
+ const endPoint = resolveMarkdownTextPoint(markdownEditorSurface, safeEnd);
2565
+
2566
+ try {
2567
+ const range = document.createRange();
2568
+ range.setStart(startPoint.node, startPoint.offset);
2569
+ range.setEnd(endPoint.node, endPoint.offset);
2570
+ return range;
2571
+ } catch {
2572
+ return null;
2573
+ }
2574
+ }
2575
+
2576
+ function toMarkdownOverlayRect(rect, surfaceRect) {
2577
+ const rawLeft = rect.left - surfaceRect.left;
2578
+ const rawTop = rect.top - surfaceRect.top;
2579
+ const rawRight = rawLeft + rect.width;
2580
+ const rawBottom = rawTop + rect.height;
2581
+
2582
+ const left = Math.max(0, rawLeft);
2583
+ const top = Math.max(0, rawTop);
2584
+ const right = Math.min(surfaceRect.width, rawRight);
2585
+ const bottom = Math.min(surfaceRect.height, rawBottom);
2586
+ const width = right - left;
2587
+ const height = bottom - top;
2588
+
2589
+ if (width <= 0 || height <= 0) {
2590
+ return null;
2591
+ }
2592
+
2593
+ return {
2594
+ left: left,
2595
+ top: top,
2596
+ width: width,
2597
+ height: height
2598
+ };
2599
+ }
2600
+
2601
+ function renderMarkdownSelectionOverlay() {
2602
+ if (!markdownSelectionOverlayRoot) {
2603
+ return;
2604
+ }
2605
+
2606
+ if (
2607
+ !markdownEditorRoot ||
2608
+ markdownEditorRoot.style.display !== 'block' ||
2609
+ !markdownEditorSurface ||
2610
+ !markdownLexicalApi ||
2611
+ !Array.isArray(markdownOverlaySelections) ||
2612
+ markdownOverlaySelections.length === 0
2613
+ ) {
2614
+ clearMarkdownSelectionOverlay();
2615
+ return;
2616
+ }
2617
+
2618
+ const surfaceRect = markdownEditorSurface.getBoundingClientRect();
2619
+ if (surfaceRect.width <= 0 || surfaceRect.height <= 0) {
2620
+ clearMarkdownSelectionOverlay();
2621
+ return;
2622
+ }
2623
+
2624
+ const computedStyle = window.getComputedStyle(markdownEditorSurface);
2625
+ const lineHeight = Math.max(14, Number.parseFloat(computedStyle.lineHeight || '0') || 22);
2626
+ markdownSelectionOverlayRoot.textContent = '';
2627
+ markdownSelectionOverlayRoot.style.display = 'block';
2628
+
2629
+ for (const selection of markdownOverlaySelections) {
2630
+ if (!selection || typeof selection.start !== 'number' || typeof selection.end !== 'number') {
2631
+ continue;
2632
+ }
2633
+
2634
+ const range = createMarkdownEditorRange(selection.start, selection.end);
2635
+ if (!range) {
2636
+ continue;
2637
+ }
2638
+
2639
+ const color = typeof selection.color === 'string' && selection.color ? selection.color : '#6b7280';
2640
+ const name = typeof selection.name === 'string' && selection.name ? selection.name : 'Anonymous';
2641
+ let labelAnchor = null;
2642
+
2643
+ if (selection.start === selection.end) {
2644
+ const caretRect = range.getBoundingClientRect();
2645
+ const clippedCaret = toMarkdownOverlayRect(
2646
+ {
2647
+ left: caretRect.left,
2648
+ top: caretRect.top,
2649
+ width: 2,
2650
+ height: Math.max(caretRect.height, lineHeight)
2651
+ },
2652
+ surfaceRect
2653
+ );
2654
+
2655
+ if (!clippedCaret) {
2656
+ continue;
2657
+ }
2658
+
2659
+ const caret = document.createElement('div');
2660
+ caret.className = 'vf-markdown-editor__selection-caret';
2661
+ caret.setAttribute(DATA_VF_IGNORE, 'true');
2662
+ caret.style.left = clippedCaret.left + 'px';
2663
+ caret.style.top = clippedCaret.top + 'px';
2664
+ caret.style.height = clippedCaret.height + 'px';
2665
+ caret.style.background = color;
2666
+ markdownSelectionOverlayRoot.appendChild(caret);
2667
+
2668
+ labelAnchor = { left: clippedCaret.left, top: clippedCaret.top };
2669
+ } else {
2670
+ const rectList = Array.from(range.getClientRects());
2671
+ for (const rect of rectList) {
2672
+ const clippedRect = toMarkdownOverlayRect(rect, surfaceRect);
2673
+ if (!clippedRect) {
2674
+ continue;
2675
+ }
2676
+
2677
+ const highlight = document.createElement('div');
2678
+ highlight.className = 'vf-markdown-editor__selection-highlight';
2679
+ highlight.setAttribute(DATA_VF_IGNORE, 'true');
2680
+ highlight.style.left = clippedRect.left + 'px';
2681
+ highlight.style.top = clippedRect.top + 'px';
2682
+ highlight.style.width = clippedRect.width + 'px';
2683
+ highlight.style.height = clippedRect.height + 'px';
2684
+ highlight.style.background = color;
2685
+ markdownSelectionOverlayRoot.appendChild(highlight);
2686
+
2687
+ if (!labelAnchor) {
2688
+ labelAnchor = { left: clippedRect.left, top: clippedRect.top };
2689
+ }
2690
+ }
2691
+ }
2692
+
2693
+ if (!labelAnchor) {
2694
+ continue;
2695
+ }
2696
+
2697
+ const label = document.createElement('div');
2698
+ label.className = 'vf-markdown-editor__selection-label';
2699
+ label.setAttribute(DATA_VF_IGNORE, 'true');
2700
+ label.textContent = name;
2701
+ label.style.left = labelAnchor.left + 'px';
2702
+ label.style.top = labelAnchor.top + 'px';
2703
+ label.style.background = color;
2704
+ markdownSelectionOverlayRoot.appendChild(label);
2705
+ }
2706
+
2707
+ if (markdownSelectionOverlayRoot.childNodes.length === 0) {
2708
+ clearMarkdownSelectionOverlay();
2709
+ }
2710
+ }
2711
+
2712
+ function scheduleMarkdownSelectionOverlayRender() {
2713
+ if (markdownSelectionOverlayRenderFrame) {
2714
+ cancelAnimationFrame(markdownSelectionOverlayRenderFrame);
2715
+ }
2716
+
2717
+ markdownSelectionOverlayRenderFrame = requestAnimationFrame(function() {
2718
+ markdownSelectionOverlayRenderFrame = null;
2719
+ renderMarkdownSelectionOverlay();
2720
+ });
2721
+ }
2722
+
2723
+ function extractMarkdownParts(content) {
2724
+ if (typeof content !== 'string') {
2725
+ return {
2726
+ frontmatter: '',
2727
+ body: ''
2728
+ };
2729
+ }
2730
+
2731
+ const frontmatterPattern = new RegExp(
2732
+ '^---[ \\\\t]*\\\\r?\\\\n[\\\\s\\\\S]*?\\\\r?\\\\n---[ \\\\t]*(?:\\\\r?\\\\n)?'
2733
+ );
2734
+ const match = content.match(frontmatterPattern);
2735
+ if (!match) {
2736
+ return {
2737
+ frontmatter: '',
2738
+ body: content
2739
+ };
2740
+ }
2741
+
2742
+ return {
2743
+ frontmatter: match[0],
2744
+ body: content.slice(match[0].length)
2745
+ };
2746
+ }
2747
+
2748
+ function composeMarkdownContent(body) {
2749
+ const safeBody = typeof body === 'string' ? body : '';
2750
+ if (!markdownFrontmatter) {
2751
+ return safeBody;
2752
+ }
2753
+ if (!safeBody) {
2754
+ return markdownFrontmatter;
2755
+ }
2756
+ if (markdownFrontmatter.endsWith('\\n')) {
2757
+ return markdownFrontmatter + safeBody;
2758
+ }
2759
+ return markdownFrontmatter + '\\n' + safeBody;
2760
+ }
2761
+
2762
+ function extractRawBlocksForEditor(body, mdxImportMap) {
2763
+ const source = typeof body === 'string' ? body : '';
2764
+ const rawBlocks = [];
2765
+ const mdxBlocks = [];
2766
+ const tokenPrefix = 'VF_RAW_BLOCK_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
2767
+ const trackMdxBlocks = isMdxPage();
2768
+ const importMap = mdxImportMap && typeof mdxImportMap === 'object' ? mdxImportMap : {};
2769
+ const createToken = function(index) {
2770
+ return '[[' + tokenPrefix + '_' + index + ']]';
2771
+ };
2772
+ const registerMdxBlock = function(rawBlock, tokenIndex, offset, inputText) {
2773
+ if (!trackMdxBlocks) {
2774
+ return;
2775
+ }
2776
+
2777
+ const trimmed = String(rawBlock || '').trimStart();
2778
+ const fence = String.fromCharCode(96, 96, 96);
2779
+ const startsWithTsxFence = trimmed.startsWith(fence + 'tsx') || trimmed.startsWith(fence + 'jsx');
2780
+ const startsWithUpperTag = /^<\\s*[A-Z]/.test(trimmed);
2781
+ const hasTsxProps = trimmed.indexOf('{') >= 0 && trimmed.indexOf('}') >= 0;
2782
+
2783
+ if (!startsWithTsxFence && !startsWithUpperTag && !hasTsxProps) {
2784
+ return;
2785
+ }
2786
+
2787
+ const label = startsWithTsxFence
2788
+ ? 'TSX block'
2789
+ : 'JSX ' + getMdxComponentName(trimmed);
2790
+ const componentName = getMdxComponentName(trimmed);
2791
+ const componentNamePattern = /^[A-Z][\\w$]*(?:\\.[A-Z][\\w$]*)*$/;
2792
+ const normalizedComponentName = componentNamePattern.test(componentName) ? componentName : '';
2793
+ const componentParts = normalizedComponentName ? normalizedComponentName.split('.') : [];
2794
+ const namespaceName = componentParts.length > 0 ? componentParts[0] : '';
2795
+ const fallbackSymbol = componentParts.length > 0 ? componentParts[componentParts.length - 1] : '';
2796
+ const directEntry = normalizedComponentName ? importMap[normalizedComponentName] : null;
2797
+ const namespaceEntry = !directEntry && namespaceName ? importMap[namespaceName] : null;
2798
+ const importEntry = directEntry || namespaceEntry || null;
2799
+ const entryPath = importEntry && typeof importEntry.filePath === 'string'
2800
+ ? importEntry.filePath
2801
+ : (
2802
+ typeof importEntry === 'string'
2803
+ ? importEntry
2804
+ : ''
2805
+ );
2806
+ const entrySymbol = importEntry && typeof importEntry.symbolName === 'string'
2807
+ ? importEntry.symbolName
2808
+ : '';
2809
+ const entryKind = importEntry && typeof importEntry.importKind === 'string'
2810
+ ? importEntry.importKind
2811
+ : '';
2812
+ const componentPath = entryPath || '';
2813
+ let componentSymbol = fallbackSymbol;
2814
+ if (entrySymbol) {
2815
+ componentSymbol = entrySymbol;
2816
+ } else if (entryKind === 'namespace' && componentParts.length > 1) {
2817
+ componentSymbol = componentParts[componentParts.length - 1];
2818
+ }
2819
+ mdxBlocks.push({
2820
+ tokenIndex: tokenIndex,
2821
+ label: label,
2822
+ lineNumber: getLineNumberForOffset(inputText, offset),
2823
+ filePath: componentPath,
2824
+ symbolName: componentSymbol
2825
+ });
2826
+ };
2827
+ const replaceWithToken = function(match, leadingNewline, offset, inputText) {
2828
+ const safeLeading = typeof leadingNewline === 'string' ? leadingNewline : '';
2829
+ const tokenIndex = rawBlocks.length;
2830
+ const rawBlock = typeof match === 'string' ? match.trimStart() : '';
2831
+ rawBlocks.push(rawBlock);
2832
+ registerMdxBlock(rawBlock, tokenIndex, Math.max(0, (offset || 0) + safeLeading.length), inputText || source);
2833
+ return safeLeading + createToken(tokenIndex);
2834
+ };
2835
+
2836
+ const mermaidFencePattern = new RegExp(
2837
+ '(^|\\\\n)\\\\x60\\\\x60\\\\x60mermaid[^\\\\n]*\\\\n[\\\\s\\\\S]*?\\\\n\\\\x60\\\\x60\\\\x60(?=\\\\n|$)',
2838
+ 'g'
2839
+ );
2840
+ const tsxFencePattern = new RegExp(
2841
+ '(^|\\\\n)\\\\x60\\\\x60\\\\x60(?:tsx|jsx)[^\\\\n]*\\\\n[\\\\s\\\\S]*?\\\\n\\\\x60\\\\x60\\\\x60(?=\\\\n|$)',
2842
+ 'g'
2843
+ );
2844
+ const htmlBlockPattern = new RegExp(
2845
+ '(^|\\\\n)<[A-Za-z][\\\\w:-]*(?:\\\\s[^>\\\\n]*)?>[\\\\s\\\\S]*?<\\\\/[A-Za-z][\\\\w:-]*>(?=\\\\n|$)',
2846
+ 'g'
2847
+ );
2848
+ const htmlSelfClosingPattern = new RegExp(
2849
+ '(^|\\\\n)<[A-Za-z][\\\\w:-]*(?:\\\\s[^>\\\\n]*)?\\\\/>(?=\\\\n|$)',
2850
+ 'g'
2851
+ );
2852
+
2853
+ let editorBody = source.replace(
2854
+ mermaidFencePattern,
2855
+ replaceWithToken
2856
+ );
2857
+
2858
+ editorBody = editorBody.replace(
2859
+ tsxFencePattern,
2860
+ replaceWithToken
2861
+ );
2862
+
2863
+ editorBody = editorBody.replace(
2864
+ htmlBlockPattern,
2865
+ replaceWithToken
2866
+ );
2867
+
2868
+ editorBody = editorBody.replace(
2869
+ htmlSelfClosingPattern,
2870
+ replaceWithToken
2871
+ );
2872
+
2873
+ return {
2874
+ editorBody: editorBody,
2875
+ rawBlocks: rawBlocks,
2876
+ mdxBlocks: mdxBlocks,
2877
+ tokenPrefix: tokenPrefix
2878
+ };
2879
+ }
2880
+
2881
+ function restoreRawBlocksFromEditor(editorBody) {
2882
+ const source = typeof editorBody === 'string' ? editorBody : '';
2883
+ if (!source || markdownRawBlocks.length === 0) {
2884
+ return source;
2885
+ }
2886
+ const rawBlockTokenPattern = getMarkdownRawBlockTokenPattern();
2887
+
2888
+ return source.replace(rawBlockTokenPattern, function(match, indexText) {
2889
+ const index = Number(indexText);
2890
+ if (!Number.isInteger(index) || index < 0 || index >= markdownRawBlocks.length) {
2891
+ return match;
2892
+ }
2893
+ const rawBlock = markdownRawBlocks[index];
2894
+ return typeof rawBlock === 'string' ? rawBlock : match;
2895
+ });
2896
+ }
2897
+
2898
+ function handleMarkdownLocalChange(content) {
2899
+ if (typeof content !== 'string') {
2900
+ return;
2901
+ }
2902
+
2903
+ markdownCurrentEditorContent = content;
2904
+ const restoredBody = restoreRawBlocksFromEditor(content);
2905
+ const fullContent = composeMarkdownContent(restoredBody);
2906
+ if (fullContent === markdownCurrentContent) {
2907
+ return;
2908
+ }
2909
+ markdownCurrentContent = fullContent;
2910
+ markdownHasUnsavedChanges = true;
2911
+ scheduleMarkdownSync(fullContent);
2912
+ scheduleMarkdownSelectionOverlayRender();
2913
+ }
2914
+
2915
+ function saveMarkdownContent() {
2916
+ if (!markdownHasUnsavedChanges) {
2917
+ return;
2918
+ }
2919
+ setMarkdownPersistStatus('saving');
2920
+ if (markdownSyncTimer) {
2921
+ clearTimeout(markdownSyncTimer);
2922
+ markdownSyncTimer = null;
2923
+ }
2924
+ postToStudio({
2925
+ action: 'markdownContentChange',
2926
+ fileId: markdownFileId,
2927
+ filePath: PAGE_PATH,
2928
+ content: markdownCurrentContent,
2929
+ save: true
2930
+ });
2931
+ markdownHasUnsavedChanges = false;
2932
+ }
2933
+
2934
+ function setupMarkdownLexicalEditor() {
2935
+ if (!markdownEditorSurface || markdownLexicalApi || markdownLexicalSetupPromise) {
2936
+ return;
2937
+ }
2938
+
2939
+ markdownLexicalSetupPromise = Promise.all([
2940
+ import('https://esm.sh/lexical@0.21.0?target=es2022'),
2941
+ import('https://esm.sh/@lexical/rich-text@0.21.0?target=es2022'),
2942
+ import('https://esm.sh/@lexical/list@0.21.0?target=es2022'),
2943
+ import('https://esm.sh/@lexical/markdown@0.21.0?target=es2022'),
2944
+ import('https://esm.sh/@lexical/history@0.21.0?target=es2022')
2945
+ ]).then(function(modules) {
2946
+ if (!markdownEditorSurface) {
2947
+ return;
2948
+ }
2949
+
2950
+ const lexicalModule = modules[0];
2951
+ const richTextModule = modules[1];
2952
+ const listModule = modules[2];
2953
+ const markdownModule = modules[3];
2954
+ const historyModule = modules[4];
2955
+
2956
+ const editor = lexicalModule.createEditor({
2957
+ namespace: 'veryfront-markdown-preview',
2958
+ nodes: [
2959
+ richTextModule.HeadingNode,
2960
+ richTextModule.QuoteNode,
2961
+ listModule.ListNode,
2962
+ listModule.ListItemNode
2963
+ ],
2964
+ onError: function(error) {
2965
+ console.error('[StudioBridge] Markdown Lexical error', error);
2966
+ }
2967
+ });
2968
+
2969
+ const unregisterRichText = richTextModule.registerRichText(editor);
2970
+ const unregisterList = listModule.registerList(editor);
2971
+ const unregisterHistory = historyModule.registerHistory(
2972
+ editor,
2973
+ historyModule.createEmptyHistoryState(),
2974
+ 1000
2975
+ );
2976
+ const unregisterUpdate = editor.registerUpdateListener(function(update) {
2977
+ if (markdownApplyingRemoteUpdate) {
2978
+ return;
2979
+ }
2980
+
2981
+ let nextContent = '';
2982
+ update.editorState.read(function() {
2983
+ nextContent = markdownModule.$convertToMarkdownString(markdownModule.TRANSFORMERS, true);
2984
+ });
2985
+ const restoredBody = restoreRawBlocksFromEditor(nextContent);
2986
+ const fullContent = composeMarkdownContent(restoredBody);
2987
+
2988
+ if (fullContent === markdownLexicalRenderedContent) {
2989
+ return;
2990
+ }
2991
+ markdownLexicalRenderedContent = fullContent;
2992
+
2993
+ handleMarkdownLocalChange(nextContent);
2994
+ scheduleMarkdownSlashMenuUpdate();
2995
+ scheduleMarkdownInlineToolbarUpdate();
2996
+ });
2997
+
2998
+ editor.setRootElement(markdownEditorSurface);
2999
+ editor.update(function() {
3000
+ const root = lexicalModule.$getRoot();
3001
+ if (root.getChildrenSize() === 0) {
3002
+ root.append(lexicalModule.$createParagraphNode());
3003
+ }
3004
+ });
3005
+ markdownLexicalApi = {
3006
+ editor: editor,
3007
+ lexicalModule: lexicalModule,
3008
+ markdownModule: markdownModule,
3009
+ unregisterRichText: unregisterRichText,
3010
+ unregisterList: unregisterList,
3011
+ unregisterHistory: unregisterHistory,
3012
+ unregisterUpdate: unregisterUpdate
3013
+ };
3014
+
3015
+ markdownEditorSurface.style.display = 'block';
3016
+ if (markdownEditorTextarea) {
3017
+ markdownEditorTextarea.style.display = 'none';
3018
+ }
3019
+
3020
+ applyMarkdownContent(markdownCurrentContent);
3021
+ hideMarkdownBlockDropIndicator();
3022
+ }).catch(function(error) {
3023
+ console.warn(
3024
+ '[StudioBridge] Failed to load Lexical markdown editor; falling back to textarea',
3025
+ error
3026
+ );
3027
+ if (markdownEditorSurface) {
3028
+ markdownEditorSurface.style.display = 'none';
3029
+ }
3030
+ if (markdownEditorTextarea) {
3031
+ markdownEditorTextarea.style.display = 'block';
3032
+ }
3033
+ hideMarkdownSlashMenu();
3034
+ hideMarkdownInlineToolbar();
3035
+ hideMarkdownBlockDragUi();
3036
+ clearMarkdownSelectionOverlay();
3037
+ });
3038
+ }
3039
+
3040
+ function focusMarkdownEditor() {
3041
+ if (markdownLexicalApi && markdownEditorSurface) {
3042
+ markdownEditorSurface.focus();
3043
+ return;
3044
+ }
3045
+ if (markdownEditorTextarea) {
3046
+ markdownEditorTextarea.focus();
3047
+ }
3048
+ }
3049
+
3050
+ function applyMarkdownHistoryCommand(command) {
3051
+ if (!markdownLexicalApi || !markdownLexicalApi.editor || !markdownLexicalApi.lexicalModule) {
3052
+ return;
3053
+ }
3054
+ if (!command) {
3055
+ return;
3056
+ }
3057
+
3058
+ markdownLexicalApi.editor.focus();
3059
+ markdownLexicalApi.editor.dispatchCommand(command, undefined);
3060
+ scheduleMarkdownSelectionSync();
3061
+ scheduleMarkdownSelectionOverlayRender();
3062
+ scheduleMarkdownSlashMenuUpdate();
3063
+ scheduleMarkdownInlineToolbarUpdate();
3064
+ }
3065
+
3066
+ function applyMarkdownContent(content) {
3067
+ if (typeof content !== 'string') {
3068
+ return;
3069
+ }
3070
+
3071
+ if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
3072
+ markdownCurrentContent = content;
3073
+ scheduleMarkdownSelectionOverlayRender();
3074
+ scheduleMarkdownSlashMenuUpdate();
3075
+ scheduleMarkdownInlineToolbarUpdate();
3076
+ hideMarkdownBlockDropIndicator();
3077
+ return;
3078
+ }
3079
+
3080
+ const mdxImportMap = parseMdxImportMap(content);
3081
+ const parts = extractMarkdownParts(content);
3082
+ const extracted = extractRawBlocksForEditor(parts.body, mdxImportMap);
3083
+ const editorContent = extracted.editorBody;
3084
+ const mdxBlocks = Array.isArray(extracted.mdxBlocks) ? extracted.mdxBlocks : [];
3085
+ markdownFrontmatter = parts.frontmatter;
3086
+ markdownRawBlocks = extracted.rawBlocks;
3087
+ markdownRawBlockTokenPrefix = extracted.tokenPrefix;
3088
+ markdownLatestMdxImportMap = mdxImportMap;
3089
+ setMarkdownMdxBlocks(mdxBlocks);
3090
+
3091
+ markdownCurrentContent = content;
3092
+ markdownCurrentEditorContent = editorContent;
3093
+
3094
+ if (markdownLexicalApi) {
3095
+ markdownApplyingRemoteUpdate = true;
3096
+ try {
3097
+ markdownLexicalRenderedContent = content;
3098
+ markdownLexicalApi.editor.update(function() {
3099
+ const lexicalModule = markdownLexicalApi.lexicalModule;
3100
+ const markdownModule = markdownLexicalApi.markdownModule;
3101
+ const root = lexicalModule.$getRoot();
3102
+ root.clear();
3103
+ markdownModule.$convertFromMarkdownString(editorContent, markdownModule.TRANSFORMERS, true);
3104
+ if (root.getChildrenSize() === 0) {
3105
+ root.append(lexicalModule.$createParagraphNode());
3106
+ }
3107
+ }, { discrete: true });
3108
+ } finally {
3109
+ markdownApplyingRemoteUpdate = false;
3110
+ }
3111
+ scheduleMarkdownSelectionOverlayRender();
3112
+ scheduleMarkdownSlashMenuUpdate();
3113
+ scheduleMarkdownInlineToolbarUpdate();
3114
+ hideMarkdownBlockDropIndicator();
3115
+ return;
3116
+ }
3117
+
3118
+ if (!markdownEditorTextarea || markdownEditorTextarea.value === editorContent) {
3119
+ clearMarkdownSelectionOverlay();
3120
+ hideMarkdownSlashMenu();
3121
+ hideMarkdownInlineToolbar();
3122
+ hideMarkdownBlockDragUi();
3123
+ return;
3124
+ }
3125
+
3126
+ const selectionStart = markdownEditorTextarea.selectionStart;
3127
+ const selectionEnd = markdownEditorTextarea.selectionEnd;
3128
+ markdownEditorTextarea.value = editorContent;
3129
+
3130
+ if (typeof selectionStart === 'number' && typeof selectionEnd === 'number') {
3131
+ const max = markdownEditorTextarea.value.length;
3132
+ markdownEditorTextarea.setSelectionRange(Math.min(selectionStart, max), Math.min(selectionEnd, max));
3133
+ }
3134
+ clearMarkdownSelectionOverlay();
3135
+ hideMarkdownSlashMenu();
3136
+ hideMarkdownInlineToolbar();
3137
+ hideMarkdownBlockDragUi();
3138
+ }
3139
+
3140
+ function setMarkdownPersistStatus(status) {
3141
+ if (!markdownPersistStatus) {
3142
+ return;
3143
+ }
3144
+
3145
+ const nextStatus = status === 'saving' || status === 'saved' || status === 'error'
3146
+ ? status
3147
+ : 'saved';
3148
+
3149
+ markdownPersistStatus.setAttribute('data-state', nextStatus);
3150
+ if (nextStatus === 'saving') {
3151
+ markdownPersistStatus.textContent = 'Saving...';
3152
+ return;
3153
+ }
3154
+ if (nextStatus === 'error') {
3155
+ markdownPersistStatus.textContent = 'Save failed';
3156
+ return;
3157
+ }
3158
+ markdownPersistStatus.textContent = 'Saved';
3159
+ }
3160
+
3161
+ function setMarkdownPresence(users) {
3162
+ markdownLatestPresenceUsers = Array.isArray(users) ? users : [];
3163
+ if (!markdownPresenceRoot) {
3164
+ return;
3165
+ }
3166
+
3167
+ markdownPresenceRoot.textContent = '';
3168
+ if (!Array.isArray(users) || users.length === 0) {
3169
+ markdownPresenceRoot.style.display = 'none';
3170
+ return;
3171
+ }
3172
+
3173
+ const visibleUsers = users.filter(function(user) {
3174
+ return user && typeof user.name === 'string';
3175
+ }).slice(0, 4);
3176
+
3177
+ if (visibleUsers.length === 0) {
3178
+ markdownPresenceRoot.style.display = 'none';
3179
+ return;
3180
+ }
3181
+
3182
+ markdownPresenceRoot.style.display = 'inline-flex';
3183
+
3184
+ for (const user of visibleUsers) {
3185
+ const pill = document.createElement('div');
3186
+ pill.className = 'vf-markdown-editor__presence-pill';
3187
+ pill.setAttribute(DATA_VF_IGNORE, 'true');
3188
+ pill.setAttribute('data-current', user.isCurrentUser ? 'true' : 'false');
3189
+ pill.setAttribute('data-agent', user.isAgent ? 'true' : 'false');
3190
+ pill.textContent = user.isCurrentUser ? 'You' : user.name;
3191
+
3192
+ const color = typeof user.color === 'string' && user.color ? user.color : '#6b7280';
3193
+ pill.style.borderLeftColor = color;
3194
+
3195
+ markdownPresenceRoot.appendChild(pill);
3196
+ }
3197
+
3198
+ if (users.length > visibleUsers.length) {
3199
+ const extra = document.createElement('div');
3200
+ extra.className = 'vf-markdown-editor__presence-pill';
3201
+ extra.setAttribute(DATA_VF_IGNORE, 'true');
3202
+ extra.textContent = '+' + String(users.length - visibleUsers.length);
3203
+ markdownPresenceRoot.appendChild(extra);
3204
+ }
3205
+ }
3206
+
3207
+ function setMarkdownSelections(selections) {
3208
+ markdownLatestSelections = Array.isArray(selections) ? selections : [];
3209
+ if (!markdownSelectionsRoot) {
3210
+ return;
3211
+ }
3212
+
3213
+ markdownSelectionsRoot.textContent = '';
3214
+ markdownOverlaySelections = [];
3215
+ if (!Array.isArray(selections) || selections.length === 0) {
3216
+ markdownSelectionsRoot.style.display = 'none';
3217
+ clearMarkdownSelectionOverlay();
3218
+ return;
3219
+ }
3220
+
3221
+ const visibleSelections = selections.filter(function(selection) {
3222
+ return (
3223
+ selection &&
3224
+ typeof selection.name === 'string' &&
3225
+ typeof selection.start === 'number' &&
3226
+ typeof selection.end === 'number'
3227
+ );
3228
+ }).slice(0, 4);
3229
+
3230
+ if (visibleSelections.length === 0) {
3231
+ markdownSelectionsRoot.style.display = 'none';
3232
+ clearMarkdownSelectionOverlay();
3233
+ return;
3234
+ }
3235
+
3236
+ markdownSelectionsRoot.style.display = 'inline-flex';
3237
+
3238
+ for (const selection of visibleSelections) {
3239
+ const pill = document.createElement('div');
3240
+ pill.className = 'vf-markdown-editor__selection-pill';
3241
+ pill.setAttribute(DATA_VF_IGNORE, 'true');
3242
+ const color = typeof selection.color === 'string' && selection.color ? selection.color : '#6b7280';
3243
+ const displayName = selection.isCurrentUser ? 'You' : selection.name;
3244
+ pill.style.borderLeftColor = color;
3245
+
3246
+ const start = Math.max(0, Math.trunc(selection.start));
3247
+ const end = Math.max(0, Math.trunc(selection.end));
3248
+ const rangeLabel = start === end ? '@' + String(start) : String(start) + '-' + String(end);
3249
+ pill.textContent = displayName + ' ' + rangeLabel;
3250
+ markdownSelectionsRoot.appendChild(pill);
3251
+
3252
+ const editorRange = sourceSelectionToEditorRange(start, end);
3253
+ if (!editorRange) {
3254
+ continue;
3255
+ }
3256
+
3257
+ markdownOverlaySelections.push({
3258
+ name: displayName,
3259
+ color: color,
3260
+ start: editorRange.start,
3261
+ end: editorRange.end
3262
+ });
3263
+ }
3264
+
3265
+ if (selections.length > visibleSelections.length) {
3266
+ const extra = document.createElement('div');
3267
+ extra.className = 'vf-markdown-editor__selection-pill';
3268
+ extra.setAttribute(DATA_VF_IGNORE, 'true');
3269
+ extra.textContent = '+' + String(selections.length - visibleSelections.length);
3270
+ markdownSelectionsRoot.appendChild(extra);
3271
+ }
3272
+
3273
+ scheduleMarkdownSelectionOverlayRender();
3274
+ }
3275
+
3276
+ function ensureMarkdownEditor() {
3277
+ if (markdownEditorRoot) {
3278
+ return markdownEditorRoot;
3279
+ }
3280
+
3281
+ const editorRoot = document.createElement('div');
3282
+ editorRoot.className = 'vf-markdown-editor';
3283
+ editorRoot.setAttribute(DATA_VF_IGNORE, 'true');
3284
+
3285
+ const toolbar = document.createElement('div');
3286
+ toolbar.className = 'vf-markdown-editor__toolbar';
3287
+ toolbar.setAttribute(DATA_VF_IGNORE, 'true');
3288
+
3289
+ const title = document.createElement('div');
3290
+ title.className = 'vf-markdown-editor__title';
3291
+ title.setAttribute(DATA_VF_IGNORE, 'true');
3292
+
3293
+ const titleMain = document.createElement('div');
3294
+ titleMain.className = 'vf-markdown-editor__title-main';
3295
+ titleMain.setAttribute(DATA_VF_IGNORE, 'true');
3296
+ titleMain.textContent = 'Markdown editor';
3297
+
3298
+ const titleHints = document.createElement('div');
3299
+ titleHints.className = 'vf-markdown-editor__title-hints';
3300
+ titleHints.setAttribute(DATA_VF_IGNORE, 'true');
3301
+ titleHints.textContent = '/ commands | Shift+Alt+Up/Down move block | Undo/Redo';
3302
+
3303
+ title.appendChild(titleMain);
3304
+ title.appendChild(titleHints);
3305
+
3306
+ const actions = document.createElement('div');
3307
+ actions.className = 'vf-markdown-editor__actions';
3308
+ actions.setAttribute(DATA_VF_IGNORE, 'true');
3309
+
3310
+ const status = document.createElement('div');
3311
+ status.className = 'vf-markdown-editor__status';
3312
+ status.setAttribute(DATA_VF_IGNORE, 'true');
3313
+ status.textContent = '';
3314
+ status.setAttribute('data-state', '');
3315
+
3316
+ const presence = document.createElement('div');
3317
+ presence.className = 'vf-markdown-editor__presence';
3318
+ presence.setAttribute(DATA_VF_IGNORE, 'true');
3319
+
3320
+ const selections = document.createElement('div');
3321
+ selections.className = 'vf-markdown-editor__selections';
3322
+ selections.setAttribute(DATA_VF_IGNORE, 'true');
3323
+
3324
+ const undoButton = document.createElement('button');
3325
+ undoButton.type = 'button';
3326
+ undoButton.className = 'vf-markdown-editor__history';
3327
+ undoButton.setAttribute(DATA_VF_IGNORE, 'true');
3328
+ undoButton.setAttribute('title', 'Undo');
3329
+ undoButton.textContent = 'Undo';
3330
+ undoButton.addEventListener('click', function() {
3331
+ if (!markdownLexicalApi || !markdownLexicalApi.lexicalModule) {
3332
+ return;
3333
+ }
3334
+ applyMarkdownHistoryCommand(markdownLexicalApi.lexicalModule.UNDO_COMMAND);
3335
+ });
3336
+
3337
+ const redoButton = document.createElement('button');
3338
+ redoButton.type = 'button';
3339
+ redoButton.className = 'vf-markdown-editor__history';
3340
+ redoButton.setAttribute(DATA_VF_IGNORE, 'true');
3341
+ redoButton.setAttribute('title', 'Redo');
3342
+ redoButton.textContent = 'Redo';
3343
+ redoButton.addEventListener('click', function() {
3344
+ if (!markdownLexicalApi || !markdownLexicalApi.lexicalModule) {
3345
+ return;
3346
+ }
3347
+ applyMarkdownHistoryCommand(markdownLexicalApi.lexicalModule.REDO_COMMAND);
3348
+ });
3349
+
3350
+ const openStudioButton = document.createElement('button');
3351
+ openStudioButton.type = 'button';
3352
+ openStudioButton.className = 'vf-markdown-editor__history';
3353
+ openStudioButton.setAttribute(DATA_VF_IGNORE, 'true');
3354
+ openStudioButton.setAttribute('title', 'Open file in Studio');
3355
+ openStudioButton.textContent = 'Open';
3356
+ openStudioButton.addEventListener('click', function() {
3357
+ openMarkdownSourceInStudio(1);
3358
+ });
3359
+
3360
+ const exitButton = document.createElement('button');
3361
+ exitButton.type = 'button';
3362
+ exitButton.className = 'vf-markdown-editor__exit';
3363
+ exitButton.setAttribute(DATA_VF_IGNORE, 'true');
3364
+ exitButton.textContent = 'Done';
3365
+ exitButton.addEventListener('click', function() {
3366
+ setMarkdownEditMode(false);
3367
+ });
3368
+
3369
+ actions.appendChild(status);
3370
+ actions.appendChild(presence);
3371
+ actions.appendChild(selections);
3372
+ actions.appendChild(undoButton);
3373
+ actions.appendChild(redoButton);
3374
+ actions.appendChild(openStudioButton);
3375
+ actions.appendChild(exitButton);
3376
+
3377
+ toolbar.appendChild(title);
3378
+ toolbar.appendChild(actions);
3379
+
3380
+ const mdxBlocks = document.createElement('div');
3381
+ mdxBlocks.className = 'vf-markdown-editor__mdx-blocks';
3382
+ mdxBlocks.setAttribute(DATA_VF_IGNORE, 'true');
3383
+
3384
+ const surface = document.createElement('div');
3385
+ surface.className = 'vf-markdown-editor__surface markdown-body';
3386
+ surface.setAttribute(DATA_VF_IGNORE, 'true');
3387
+ surface.setAttribute('contenteditable', 'true');
3388
+ surface.setAttribute('aria-label', 'Markdown editor');
3389
+ surface.addEventListener('keyup', scheduleMarkdownSelectionSync);
3390
+ surface.addEventListener('mouseup', scheduleMarkdownSelectionSync);
3391
+ surface.addEventListener('input', function() {
3392
+ scheduleMarkdownSelectionSync();
3393
+ scheduleMarkdownSlashMenuUpdate();
3394
+ });
3395
+ surface.addEventListener('keyup', scheduleMarkdownSlashMenuUpdate);
3396
+ surface.addEventListener('mouseup', scheduleMarkdownSlashMenuUpdate);
3397
+ surface.addEventListener('keydown', handleMarkdownSlashMenuKeydown);
3398
+ surface.addEventListener('keydown', function(event) {
3399
+ if (!event.shiftKey || !event.altKey) {
3400
+ return;
3401
+ }
3402
+ if (event.key === 'ArrowUp') {
3403
+ const moved = moveMarkdownCurrentBlockByDelta(-1);
3404
+ if (moved) {
3405
+ event.preventDefault();
3406
+ }
3407
+ return;
3408
+ }
3409
+ if (event.key === 'ArrowDown') {
3410
+ const moved = moveMarkdownCurrentBlockByDelta(1);
3411
+ if (moved) {
3412
+ event.preventDefault();
3413
+ }
3414
+ }
3415
+ });
3416
+ surface.addEventListener('keydown', function(event) {
3417
+ if ((event.metaKey || event.ctrlKey) && event.key === 's') {
3418
+ event.preventDefault();
3419
+ saveMarkdownContent();
3420
+ }
3421
+ });
3422
+ surface.addEventListener('scroll', scheduleMarkdownSelectionOverlayRender);
3423
+ surface.addEventListener('scroll', scheduleMarkdownSlashMenuUpdate);
3424
+ surface.addEventListener('keyup', scheduleMarkdownInlineToolbarUpdate);
3425
+ surface.addEventListener('mouseup', scheduleMarkdownInlineToolbarUpdate);
3426
+ surface.addEventListener('input', scheduleMarkdownInlineToolbarUpdate);
3427
+ surface.addEventListener('scroll', scheduleMarkdownInlineToolbarUpdate);
3428
+ surface.addEventListener('scroll', refreshMarkdownBlockDragHandlePosition);
3429
+
3430
+ const surfaceWrap = document.createElement('div');
3431
+ surfaceWrap.className = 'vf-markdown-editor__surface-wrap';
3432
+ surfaceWrap.setAttribute(DATA_VF_IGNORE, 'true');
3433
+
3434
+ const selectionOverlay = document.createElement('div');
3435
+ selectionOverlay.className = 'vf-markdown-editor__selection-overlay';
3436
+ selectionOverlay.setAttribute(DATA_VF_IGNORE, 'true');
3437
+
3438
+ const slashMenu = document.createElement('div');
3439
+ slashMenu.className = 'vf-markdown-editor__slash-menu';
3440
+ slashMenu.setAttribute(DATA_VF_IGNORE, 'true');
3441
+
3442
+ const inlineToolbar = document.createElement('div');
3443
+ inlineToolbar.className = 'vf-markdown-editor__inline-toolbar';
3444
+ inlineToolbar.setAttribute(DATA_VF_IGNORE, 'true');
3445
+
3446
+ const boldButton = document.createElement('button');
3447
+ boldButton.type = 'button';
3448
+ boldButton.className = 'vf-markdown-editor__inline-button';
3449
+ boldButton.setAttribute(DATA_VF_IGNORE, 'true');
3450
+ boldButton.textContent = 'B';
3451
+ boldButton.addEventListener('mousedown', function(event) {
3452
+ event.preventDefault();
3453
+ });
3454
+ boldButton.addEventListener('click', function(event) {
3455
+ event.preventDefault();
3456
+ toggleMarkdownInlineFormat('bold');
3457
+ });
3458
+
3459
+ const italicButton = document.createElement('button');
3460
+ italicButton.type = 'button';
3461
+ italicButton.className = 'vf-markdown-editor__inline-button';
3462
+ italicButton.setAttribute(DATA_VF_IGNORE, 'true');
3463
+ italicButton.textContent = 'I';
3464
+ italicButton.addEventListener('mousedown', function(event) {
3465
+ event.preventDefault();
3466
+ });
3467
+ italicButton.addEventListener('click', function(event) {
3468
+ event.preventDefault();
3469
+ toggleMarkdownInlineFormat('italic');
3470
+ });
3471
+
3472
+ const codeButton = document.createElement('button');
3473
+ codeButton.type = 'button';
3474
+ codeButton.className = 'vf-markdown-editor__inline-button';
3475
+ codeButton.setAttribute(DATA_VF_IGNORE, 'true');
3476
+ codeButton.textContent = '</>';
3477
+ codeButton.addEventListener('mousedown', function(event) {
3478
+ event.preventDefault();
3479
+ });
3480
+ codeButton.addEventListener('click', function(event) {
3481
+ event.preventDefault();
3482
+ toggleMarkdownInlineFormat('code');
3483
+ });
3484
+
3485
+ inlineToolbar.appendChild(boldButton);
3486
+ inlineToolbar.appendChild(italicButton);
3487
+ inlineToolbar.appendChild(codeButton);
3488
+
3489
+ const blockDragHandle = document.createElement('button');
3490
+ blockDragHandle.type = 'button';
3491
+ blockDragHandle.className = 'vf-markdown-editor__block-handle';
3492
+ blockDragHandle.setAttribute(DATA_VF_IGNORE, 'true');
3493
+ blockDragHandle.textContent = '::';
3494
+ blockDragHandle.draggable = true;
3495
+ blockDragHandle.setAttribute('data-dragging', 'false');
3496
+ blockDragHandle.addEventListener('dragstart', function(event) {
3497
+ const indexText = blockDragHandle.getAttribute('data-block-index');
3498
+ const index = Number(indexText);
3499
+ if (!Number.isInteger(index)) {
3500
+ event.preventDefault();
3501
+ return;
3502
+ }
3503
+ markdownBlockDragSourceIndex = index;
3504
+ markdownBlockDragActive = true;
3505
+ blockDragHandle.setAttribute('data-dragging', 'true');
3506
+ if (event.dataTransfer) {
3507
+ event.dataTransfer.effectAllowed = 'move';
3508
+ event.dataTransfer.setData('text/plain', String(index));
3509
+
3510
+ const blocks = getMarkdownTopLevelBlocks();
3511
+ const block = blocks[index];
3512
+ removeMarkdownDragGhost();
3513
+ if (block) {
3514
+ const ghost = createMarkdownDragGhost(block);
3515
+ document.body.appendChild(ghost);
3516
+ markdownBlockDragGhost = ghost;
3517
+ event.dataTransfer.setDragImage(ghost, 14, 14);
3518
+ }
3519
+ }
3520
+ showMarkdownBlockDropIndicator(index);
3521
+ });
3522
+ blockDragHandle.addEventListener('mouseenter', function() {
3523
+ if (markdownBlockHandleHoverIndex >= 0) {
3524
+ blockDragHandle.style.display = 'block';
3525
+ }
3526
+ });
3527
+ blockDragHandle.addEventListener('mouseleave', function(event) {
3528
+ if (markdownBlockDragActive) {
3529
+ return;
3530
+ }
3531
+ const next = event.relatedTarget;
3532
+ if (next && markdownEditorSurface && markdownEditorSurface.contains(next)) {
3533
+ return;
3534
+ }
3535
+ hideMarkdownBlockDragHandle();
3536
+ });
3537
+ blockDragHandle.addEventListener('dragend', function() {
3538
+ hideMarkdownBlockDragUi();
3539
+ });
3540
+
3541
+ const blockDropIndicator = document.createElement('div');
3542
+ blockDropIndicator.className = 'vf-markdown-editor__block-drop-indicator';
3543
+ blockDropIndicator.setAttribute(DATA_VF_IGNORE, 'true');
3544
+
3545
+ const blockDropLabel = document.createElement('div');
3546
+ blockDropLabel.className = 'vf-markdown-editor__block-drop-label';
3547
+ blockDropLabel.setAttribute(DATA_VF_IGNORE, 'true');
3548
+
3549
+ surfaceWrap.appendChild(surface);
3550
+ surfaceWrap.appendChild(selectionOverlay);
3551
+
3552
+ const textarea = document.createElement('textarea');
3553
+ textarea.className = 'vf-markdown-editor__textarea';
3554
+ textarea.setAttribute(DATA_VF_IGNORE, 'true');
3555
+ textarea.setAttribute('aria-label', 'Markdown editor');
3556
+ textarea.spellcheck = false;
3557
+ textarea.addEventListener('input', function() {
3558
+ handleMarkdownLocalChange(textarea.value);
3559
+ scheduleMarkdownSelectionSync();
3560
+ hideMarkdownSlashMenu();
3561
+ });
3562
+ textarea.addEventListener('select', scheduleMarkdownSelectionSync);
3563
+ textarea.addEventListener('keyup', scheduleMarkdownSelectionSync);
3564
+ textarea.addEventListener('click', scheduleMarkdownSelectionSync);
3565
+ textarea.addEventListener('input', clearMarkdownSelectionOverlay);
3566
+ textarea.addEventListener('keydown', function() {
3567
+ hideMarkdownSlashMenu();
3568
+ hideMarkdownInlineToolbar();
3569
+ hideMarkdownBlockDragUi();
3570
+ });
3571
+
3572
+ surface.addEventListener('mousemove', function(event) {
3573
+ if (markdownBlockDragActive) {
3574
+ return;
3575
+ }
3576
+
3577
+ const index = getMarkdownBlockHoverIndexFromPointer(event.target, event.clientX, event.clientY);
3578
+ if (index < 0) {
3579
+ hideMarkdownBlockDragHandle();
3580
+ return;
3581
+ }
3582
+ const blocks = getMarkdownTopLevelBlocks();
3583
+ const block = blocks[index];
3584
+ if (!block) {
3585
+ hideMarkdownBlockDragHandle();
3586
+ return;
3587
+ }
3588
+ positionMarkdownBlockDragHandle(block, index);
3589
+ });
3590
+
3591
+ surface.addEventListener('mouseleave', function(event) {
3592
+ if (!markdownBlockDragActive) {
3593
+ const next = event.relatedTarget;
3594
+ if (next && markdownBlockDragHandle && (next === markdownBlockDragHandle || markdownBlockDragHandle.contains(next))) {
3595
+ return;
3596
+ }
3597
+ hideMarkdownBlockDragHandle();
3598
+ }
3599
+ });
3600
+
3601
+ surface.addEventListener('dragover', function(event) {
3602
+ if (!markdownBlockDragActive) {
3603
+ return;
3604
+ }
3605
+ event.preventDefault();
3606
+ if (event.dataTransfer) {
3607
+ event.dataTransfer.dropEffect = 'move';
3608
+ }
3609
+ autoScrollMarkdownSurfaceDuringDrag(event.clientY);
3610
+ const slotIndex = getMarkdownDropSlotIndexFromPointer(event.clientY);
3611
+ if (slotIndex >= 0) {
3612
+ showMarkdownBlockDropIndicator(slotIndex);
3613
+ }
3614
+ });
3615
+
3616
+ surface.addEventListener('drop', function(event) {
3617
+ if (!markdownBlockDragActive) {
3618
+ return;
3619
+ }
3620
+ event.preventDefault();
3621
+
3622
+ const fallbackSlot = getMarkdownDropSlotIndexFromPointer(event.clientY);
3623
+ const slotIndex = markdownBlockDropSlotIndex >= 0 ? markdownBlockDropSlotIndex : fallbackSlot;
3624
+ const sourceIndex = markdownBlockDragSourceIndex;
3625
+ hideMarkdownBlockDragUi();
3626
+ if (sourceIndex < 0 || slotIndex < 0) {
3627
+ return;
3628
+ }
3629
+ moveMarkdownLexicalBlock(sourceIndex, slotIndex);
3630
+ });
3631
+
3632
+ document.addEventListener('selectionchange', function() {
3633
+ if (!markdownEditorRoot || markdownEditorRoot.style.display !== 'block') {
3634
+ return;
3635
+ }
3636
+ scheduleMarkdownSelectionSync();
3637
+ scheduleMarkdownSelectionOverlayRender();
3638
+ scheduleMarkdownSlashMenuUpdate();
3639
+ scheduleMarkdownInlineToolbarUpdate();
3640
+ });
3641
+
3642
+ window.addEventListener('resize', scheduleMarkdownSelectionOverlayRender);
3643
+ window.addEventListener('resize', scheduleMarkdownSlashMenuUpdate);
3644
+ window.addEventListener('resize', scheduleMarkdownInlineToolbarUpdate);
3645
+ window.addEventListener('resize', hideMarkdownBlockDragUi);
3646
+
3647
+ editorRoot.appendChild(toolbar);
3648
+ editorRoot.appendChild(mdxBlocks);
3649
+ editorRoot.appendChild(surfaceWrap);
3650
+ editorRoot.appendChild(textarea);
3651
+ editorRoot.appendChild(slashMenu);
3652
+ editorRoot.appendChild(inlineToolbar);
3653
+ editorRoot.appendChild(blockDragHandle);
3654
+ editorRoot.appendChild(blockDropIndicator);
3655
+ editorRoot.appendChild(blockDropLabel);
3656
+ document.body.appendChild(editorRoot);
3657
+
3658
+ markdownEditorRoot = editorRoot;
3659
+ markdownEditorSurface = surface;
3660
+ markdownEditorTextarea = textarea;
3661
+ markdownPersistStatus = status;
3662
+ markdownPresenceRoot = presence;
3663
+ markdownSelectionsRoot = selections;
3664
+ markdownMdxBlocksRoot = mdxBlocks;
3665
+ markdownSelectionOverlayRoot = selectionOverlay;
3666
+ markdownSlashMenuRoot = slashMenu;
3667
+ markdownInlineToolbarRoot = inlineToolbar;
3668
+ markdownBlockDragHandle = blockDragHandle;
3669
+ markdownBlockDropIndicator = blockDropIndicator;
3670
+ markdownBlockDropLabel = blockDropLabel;
3671
+ setMarkdownMdxBlocks(markdownLatestMdxBlocks);
3672
+ setMarkdownPresence(markdownLatestPresenceUsers);
3673
+ setMarkdownSelections(markdownLatestSelections);
3674
+ setupMarkdownLexicalEditor();
3675
+ applyMarkdownContent(markdownCurrentContent);
3676
+
3677
+ return editorRoot;
3678
+ }
3679
+
3680
+ function setMarkdownEditMode(enabled) {
3681
+ const markdownBody = document.getElementById('markdown-body');
3682
+ if (!markdownBody || !isMarkdownPage()) {
3683
+ return;
3684
+ }
3685
+
3686
+ if (enabled) {
3687
+ ensureMarkdownEditor();
3688
+ setupMarkdownLexicalEditor();
3689
+ markdownBody.style.display = 'none';
3690
+ markdownEditorRoot.style.display = 'block';
3691
+ markdownHasUnsavedChanges = false;
3692
+ focusMarkdownEditor();
3693
+ scheduleMarkdownSelectionSync();
3694
+ scheduleMarkdownSelectionOverlayRender();
3695
+ scheduleMarkdownSlashMenuUpdate();
3696
+ scheduleMarkdownInlineToolbarUpdate();
3697
+ postMarkdownEditorReady();
3698
+ } else {
3699
+ markdownBody.style.display = '';
3700
+ if (markdownEditorRoot) {
3701
+ markdownEditorRoot.style.display = 'none';
3702
+ }
3703
+ hideMarkdownSlashMenu();
3704
+ hideMarkdownInlineToolbar();
3705
+ hideMarkdownBlockDragUi();
3706
+ markdownOverlaySelections = [];
3707
+ clearMarkdownSelectionOverlay();
3708
+ clearMarkdownSelectionSync();
3709
+ }
3710
+
3711
+ const nextUrl = new URL(window.location.href);
3712
+ if (enabled) {
3713
+ nextUrl.searchParams.set('edit', 'true');
3714
+ } else {
3715
+ nextUrl.searchParams.delete('edit');
3716
+ }
3717
+ window.history.replaceState(window.history.state, '', nextUrl.toString());
3718
+ }
3719
+
3720
+ function ensureMarkdownEditButton() {
3721
+ if (markdownEditButton || !isMarkdownPage()) {
3722
+ return;
3723
+ }
3724
+
3725
+ const button = document.createElement('button');
3726
+ button.type = 'button';
3727
+ button.className = 'vf-markdown-edit-button';
3728
+ button.textContent = 'Edit';
3729
+ button.setAttribute(DATA_VF_IGNORE, 'true');
3730
+ button.addEventListener('click', function() {
3731
+ setMarkdownEditMode(true);
3732
+ });
3733
+
3734
+ document.body.appendChild(button);
3735
+ markdownEditButton = button;
3736
+ }
3737
+
3738
+ function setupMarkdownEditor(params) {
3739
+ if (!isMarkdownPage()) {
3740
+ return;
3741
+ }
3742
+
3743
+ markdownFileId = params.get('vf_file_id') || PAGE_ID || null;
3744
+ ensureMarkdownEditButton();
3745
+
3746
+ if (params.get('edit') === 'true') {
3747
+ setMarkdownEditMode(true);
3748
+ }
3749
+ }
3750
+
3751
+ let html2canvasLoaded = false;
3752
+ let html2canvasPromise = null;
3753
+
3754
+ function loadHtml2Canvas() {
3755
+ if (html2canvasLoaded) return Promise.resolve();
3756
+ if (html2canvasPromise) return html2canvasPromise;
3757
+
3758
+ html2canvasPromise = new Promise((resolve, reject) => {
3759
+ const script = document.createElement('script');
3760
+ script.src = 'https://cdn.jsdelivr.net/npm/html2canvas-pro@2.0.0/dist/html2canvas-pro.min.js';
3761
+ script.onload = () => {
3762
+ html2canvasLoaded = true;
3763
+ resolve();
3764
+ };
3765
+ script.onerror = (event) => {
3766
+ console.warn(
3767
+ '[StudioBridge] Failed to load html2canvas script. This may be caused by CSP script-src restrictions.',
3768
+ event
3769
+ );
3770
+ reject(new Error('Failed to load html2canvas script'));
3771
+ };
3772
+ try {
3773
+ document.head.appendChild(script);
3774
+ } catch (error) {
3775
+ console.warn(
3776
+ '[StudioBridge] Failed to append html2canvas script element. This may be caused by CSP script-src restrictions.',
3777
+ error
3778
+ );
3779
+ reject(error instanceof Error ? error : new Error('Failed to append html2canvas script element'));
3780
+ }
3781
+ });
3782
+
3783
+ return html2canvasPromise;
3784
+ }
3785
+
3786
+ async function captureScreenshot(options) {
3787
+ const { scrollTo, fullPage, quality = 0.8 } = options || {};
3788
+ const originalScrollY = window.scrollY;
3789
+
3790
+ try {
3791
+ await loadHtml2Canvas();
3792
+
3793
+ if (typeof scrollTo === 'number') {
3794
+ window.scrollTo(0, scrollTo);
3795
+ await new Promise(r => setTimeout(r, 150));
3796
+ }
3797
+
3798
+ const canvasOptions = {
3799
+ useCORS: true,
3800
+ logging: false,
3801
+ scale: window.devicePixelRatio || 1
3802
+ };
3803
+
3804
+ if (fullPage) {
3805
+ canvasOptions.height = document.documentElement.scrollHeight;
3806
+ canvasOptions.windowHeight = document.documentElement.scrollHeight;
3807
+ canvasOptions.y = 0;
3808
+ window.scrollTo(0, 0);
3809
+ await new Promise(r => setTimeout(r, 100));
3810
+ }
3811
+
3812
+ const html2canvasFn = window.html2canvas.default || window.html2canvas;
3813
+ const canvas = await html2canvasFn(document.body, canvasOptions);
3814
+
3815
+ if (!canvas || canvas.width === 0 || canvas.height === 0) {
3816
+ console.error('[bridge] html2canvas produced empty canvas:', canvas?.width, 'x', canvas?.height);
3817
+ window.scrollTo(0, originalScrollY);
531
3818
  return {
532
3819
  success: false,
533
3820
  error: 'html2canvas produced empty canvas (0x0 dimensions)'
@@ -642,7 +3929,47 @@ export function generateStudioBridgeScript(options) {
642
3929
  if (!inspectMode) showHoverOverlay(message.id);
643
3930
  return;
644
3931
 
645
- case 'toggleLayout':
3932
+ case 'setMarkdownContent':
3933
+ if (!isMarkdownPage()) {
3934
+ return;
3935
+ }
3936
+ if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
3937
+ return;
3938
+ }
3939
+ applyMarkdownContent(message.content || '');
3940
+ return;
3941
+
3942
+ case 'setMarkdownPersistState':
3943
+ if (!isMarkdownPage()) {
3944
+ return;
3945
+ }
3946
+ if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
3947
+ return;
3948
+ }
3949
+ setMarkdownPersistStatus(message.status || 'saved');
3950
+ if (message.status === 'saved') {
3951
+ markdownHasUnsavedChanges = false;
3952
+ }
3953
+ return;
3954
+
3955
+ case 'setMarkdownPresence':
3956
+ if (!isMarkdownPage()) {
3957
+ return;
3958
+ }
3959
+ if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
3960
+ return;
3961
+ }
3962
+ setMarkdownPresence(message.users);
3963
+ return;
3964
+
3965
+ case 'setMarkdownSelections':
3966
+ if (!isMarkdownPage()) {
3967
+ return;
3968
+ }
3969
+ if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
3970
+ return;
3971
+ }
3972
+ setMarkdownSelections(message.selections);
646
3973
  return;
647
3974
 
648
3975
  case 'screenshot':
@@ -717,6 +4044,7 @@ export function generateStudioBridgeScript(options) {
717
4044
  setupConsoleCapture();
718
4045
  setupErrorHandling();
719
4046
  setupInspectMode();
4047
+ setupMarkdownEditor(params);
720
4048
 
721
4049
  window.addEventListener('message', handleStudioMessage);
722
4050
 
@@ -747,6 +4075,16 @@ export function generateStudioBridgeScript(options) {
747
4075
  console.debug('[StudioBridge] Initialized successfully');
748
4076
  }
749
4077
 
750
- init();
4078
+ if (DEBUG_EXPOSE_INTERNALS && typeof window !== 'undefined') {
4079
+ window.__VF_STUDIO_BRIDGE_DEBUG = {
4080
+ parseMdxImportMap: parseMdxImportMap,
4081
+ extractRawBlocksForEditor: extractRawBlocksForEditor,
4082
+ getMdxBlockOpenUiState: getMdxBlockOpenUiState
4083
+ };
4084
+ }
4085
+
4086
+ if (!DEBUG_SKIP_INIT) {
4087
+ init();
4088
+ }
751
4089
  })();`;
752
4090
  }