@refrakt-md/editor 0.8.2 → 0.8.3

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 (29) hide show
  1. package/app/dist/assets/{index-Bn8ajfVl.js → index-3MvwKRVQ.js} +1 -1
  2. package/app/dist/assets/{index-DNtuldOx.js → index-B7e694w6.js} +1 -1
  3. package/app/dist/assets/{index-xo7v6nRB.js → index-BBljOYQu.js} +1 -1
  4. package/app/dist/assets/{index-DQUOY-pF.js → index-BEGy_i8o.js} +1 -1
  5. package/app/dist/assets/{index-DNJBunzP.js → index-BGy7ixjW.js} +1 -1
  6. package/app/dist/assets/{index-dGztG-54.js → index-BaLgiiKk.js} +1 -1
  7. package/app/dist/assets/{index-D5ucdUTo.js → index-BjlNcvOf.js} +1 -1
  8. package/app/dist/assets/{index-Cgbvx23V.js → index-CKfKYVw7.js} +1 -1
  9. package/app/dist/assets/{index-BDj1XPol.js → index-COFbngzR.js} +1 -1
  10. package/app/dist/assets/{index-aPeHMqUX.js → index-CPEo_rvd.js} +1 -1
  11. package/app/dist/assets/{index-BXe1fKaT.js → index-CQDCT-XT.js} +1 -1
  12. package/app/dist/assets/{index-CXeK-dZx.js → index-CUmEjEeR.js} +1 -1
  13. package/app/dist/assets/{index-80NtMar1.js → index-CeV-Af4N.js} +1 -1
  14. package/app/dist/assets/{index-CaRBCHaX.js → index-ChbH55h5.js} +1 -1
  15. package/app/dist/assets/index-CzvG5PZT.css +1 -0
  16. package/app/dist/assets/{index-BfxTGrHB.js → index-D9-aYc3I.js} +1 -1
  17. package/app/dist/assets/{index-DGYxLhpR.js → index-DezxtfNV.js} +1 -1
  18. package/app/dist/assets/{index-DskvyNKT.js → index-DrI4IfXE.js} +1 -1
  19. package/app/dist/assets/{index-CCkzIGTi.js → index-DwfxgjnU.js} +1 -1
  20. package/app/dist/assets/index-ogrpJNou.js +555 -0
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/components/BlockEditor.svelte +381 -47
  23. package/app/src/lib/components/InlineEditor.svelte +15 -5
  24. package/app/src/lib/components/ProseBlockCard.svelte +446 -0
  25. package/app/src/lib/components/ProseEditPanel.svelte +470 -0
  26. package/app/src/lib/editor/block-parser.ts +59 -2
  27. package/package.json +6 -6
  28. package/app/dist/assets/index-B6H6LF1M.css +0 -1
  29. package/app/dist/assets/index-Cd12jZId.js +0 -479
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>refrakt editor</title>
7
- <script type="module" crossorigin src="/assets/index-Cd12jZId.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-B6H6LF1M.css">
7
+ <script type="module" crossorigin src="/assets/index-ogrpJNou.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CzvG5PZT.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
@@ -9,15 +9,25 @@
9
9
  blockLabel,
10
10
  extractRuneInner,
11
11
  rebuildRuneSource,
12
+ rebuildHeadingSource,
13
+ rebuildFenceSource,
14
+ groupIntoEditorBlocks,
12
15
  type ParsedBlock,
13
16
  type RuneBlock,
17
+ type HeadingBlock,
18
+ type FenceBlock,
19
+ type EditorBlock,
20
+ type ProseBlock,
14
21
  } from '../editor/block-parser.js';
15
22
  import { findSectionMapping, applySectionEdit, findActionMapping, applyActionEdit, findCommandMapping, applyCommandEdit, applyLanguageEdit, findImageMapping, applyImageEdit, findIconMapping, applyIconEdit, type SectionMapping, type ActionMapping, type CommandMapping, type ImageMapping, type IconMapping } from '../editor/section-mapper.js';
16
23
  import { stripInlineMarkdown } from '../editor/inline-markdown.js';
17
24
  import { editorState } from '../state/editor.svelte.js';
18
25
  import BlockCard from './BlockCard.svelte';
19
26
  import type { SectionClickInfo } from './BlockCard.svelte';
27
+ import ProseBlockCard from './ProseBlockCard.svelte';
28
+ import type { ProseElementClickInfo } from './ProseBlockCard.svelte';
20
29
  import BlockEditPanel from './BlockEditPanel.svelte';
30
+ import ProseEditPanel from './ProseEditPanel.svelte';
21
31
  import FrontmatterEditPanel from './FrontmatterEditPanel.svelte';
22
32
  import InsertBlockDialog from './InsertBlockDialog.svelte';
23
33
  import InlineEditPopover from './InlineEditPopover.svelte';
@@ -114,6 +124,7 @@
114
124
  editingFrontmatter = false;
115
125
  anchorPoint = null;
116
126
  inlineEdit = null;
127
+ proseInlineEdit = null;
117
128
  }
118
129
  });
119
130
 
@@ -124,6 +135,162 @@
124
135
  onchange(newSource);
125
136
  }
126
137
 
138
+ // ── Prose block grouping ─────────────────────────────────────
139
+
140
+ let editorBlocks: EditorBlock[] = $derived(groupIntoEditorBlocks(blocks));
141
+
142
+ /** Map an editor block index to the range of flat block indices it covers */
143
+ function editorBlockToFlatRange(editorIndex: number): [number, number] {
144
+ const eb = editorBlocks[editorIndex];
145
+ if (!eb) return [0, 0];
146
+ if (eb.type === 'prose') {
147
+ const firstChild = eb.children[0];
148
+ const lastChild = eb.children[eb.children.length - 1];
149
+ const start = blocks.indexOf(firstChild);
150
+ const end = blocks.indexOf(lastChild);
151
+ return [start, end];
152
+ }
153
+ // Rune block — find its position in the flat array
154
+ const idx = blocks.indexOf(eb as ParsedBlock);
155
+ return [idx, idx];
156
+ }
157
+
158
+ /** Map a flat block index to the corresponding editor block index */
159
+ function flatToEditorIndex(flatIndex: number): number {
160
+ let offset = 0;
161
+ for (let i = 0; i < editorBlocks.length; i++) {
162
+ const eb = editorBlocks[i];
163
+ const count = eb.type === 'prose' ? eb.children.length : 1;
164
+ if (flatIndex < offset + count) return i;
165
+ offset += count;
166
+ }
167
+ return editorBlocks.length - 1;
168
+ }
169
+
170
+ // ── Prose block operations ───────────────────────────────────
171
+
172
+ /** Update a prose block from the ProseEditPanel (structure reorder, content edit) */
173
+ function handleUpdateProseBlock(editorIndex: number, updated: ProseBlock) {
174
+ const [startFlat, endFlat] = editorBlockToFlatRange(editorIndex);
175
+ const oldChildren = blocks.slice(startFlat, endFlat + 1);
176
+ reconcileIds(oldChildren, updated.children);
177
+ blocks = [
178
+ ...blocks.slice(0, startFlat),
179
+ ...updated.children,
180
+ ...blocks.slice(endFlat + 1),
181
+ ];
182
+ syncToSource();
183
+ }
184
+
185
+ /** Handle click on a prose element (heading, paragraph, etc.) for inline editing */
186
+ function handleProseSectionClick(editorIndex: number, info: ProseElementClickInfo) {
187
+ const eb = editorBlocks[editorIndex];
188
+ if (!eb || eb.type !== 'prose') return;
189
+
190
+ const child = eb.children[info.childIndex];
191
+ if (!child) return;
192
+
193
+ const [startFlat] = editorBlockToFlatRange(editorIndex);
194
+ const flatIndex = startFlat + info.childIndex;
195
+
196
+ if (child.type === 'fence') {
197
+ const fb = child as FenceBlock;
198
+ const code = fb.code;
199
+ const language = fb.language;
200
+ const opener = child.source.split('\n')[0];
201
+ const delimMatch = opener.match(/^(`{3,}|~{3,})/);
202
+ const delimiter = delimMatch ? delimMatch[1] : '```';
203
+ commandEdit = {
204
+ blockIndex: flatIndex,
205
+ rect: info.rect,
206
+ mapping: { source: child.source, code, language, opener, delimiter },
207
+ };
208
+ return;
209
+ }
210
+
211
+ // For headings and paragraphs: inline text editing
212
+ const source = child.source.trim();
213
+ let prefix = '';
214
+ let inlineContent = source;
215
+
216
+ if (child.type === 'heading') {
217
+ const match = source.match(/^(#{1,6}\s+)(.*)/);
218
+ if (match) {
219
+ prefix = match[1];
220
+ inlineContent = match[2];
221
+ }
222
+ } else if (child.type === 'quote') {
223
+ const match = source.match(/^(>\s*)(.*)/);
224
+ if (match) {
225
+ prefix = match[1];
226
+ inlineContent = match[2];
227
+ }
228
+ }
229
+
230
+ const mapping: SectionMapping = {
231
+ dataName: child.type,
232
+ text: stripInlineMarkdown(inlineContent),
233
+ source,
234
+ sourcePrefix: prefix,
235
+ inlineSource: inlineContent,
236
+ };
237
+
238
+ proseInlineEdit = {
239
+ editorIndex,
240
+ childIndex: info.childIndex,
241
+ flatIndex,
242
+ inlineSource: inlineContent,
243
+ rect: info.rect,
244
+ mapping,
245
+ };
246
+ }
247
+
248
+ // ── Prose inline edit state ──────────────────────────────────
249
+
250
+ let proseInlineEdit: {
251
+ editorIndex: number;
252
+ childIndex: number;
253
+ flatIndex: number;
254
+ inlineSource: string;
255
+ rect: DOMRect;
256
+ mapping: SectionMapping;
257
+ } | null = $state(null);
258
+
259
+ function handleProseInlineEditChange(newInlineSource: string) {
260
+ if (!proseInlineEdit) return;
261
+ const child = blocks[proseInlineEdit.flatIndex];
262
+ if (!child) return;
263
+
264
+ const newSource = proseInlineEdit.mapping.sourcePrefix + newInlineSource;
265
+ let updated: ParsedBlock;
266
+
267
+ if (child.type === 'heading') {
268
+ const hb = child as HeadingBlock;
269
+ updated = { ...hb, text: newInlineSource, source: '' } as HeadingBlock;
270
+ (updated as HeadingBlock).source = rebuildHeadingSource(updated as HeadingBlock);
271
+ } else {
272
+ updated = { ...child, source: newSource };
273
+ }
274
+
275
+ // Update the mapping for subsequent edits
276
+ proseInlineEdit = {
277
+ ...proseInlineEdit,
278
+ inlineSource: newInlineSource,
279
+ mapping: {
280
+ ...proseInlineEdit.mapping,
281
+ text: stripInlineMarkdown(newInlineSource),
282
+ source: newSource,
283
+ inlineSource: newInlineSource,
284
+ },
285
+ };
286
+
287
+ handleUpdateBlock(proseInlineEdit.flatIndex, updated);
288
+ }
289
+
290
+ function closeProseInlineEdit() {
291
+ proseInlineEdit = null;
292
+ }
293
+
127
294
  // ── Frontmatter summary for visual mode header ──────────────
128
295
 
129
296
  let editingFrontmatter = $state(false);
@@ -146,6 +313,33 @@
146
313
  const POPOVER_WIDTH = 420;
147
314
  const POPOVER_GAP = 12;
148
315
 
316
+ /** Drag the popover by its header */
317
+ function handlePopoverDragStart(e: MouseEvent) {
318
+ const header = (e.target as HTMLElement).closest('.edit-panel__header');
319
+ if (!header || (e.target as HTMLElement).closest('button')) return;
320
+
321
+ e.preventDefault();
322
+ const popoverEl = e.currentTarget as HTMLElement;
323
+ const rect = popoverEl.getBoundingClientRect();
324
+ const startX = e.clientX;
325
+ const startY = e.clientY;
326
+ const origLeft = rect.left;
327
+ const origTop = rect.top;
328
+
329
+ function onMove(ev: MouseEvent) {
330
+ popoverEl.style.left = `${origLeft + (ev.clientX - startX)}px`;
331
+ popoverEl.style.top = `${origTop + (ev.clientY - startY)}px`;
332
+ }
333
+
334
+ function onUp() {
335
+ window.removeEventListener('mousemove', onMove);
336
+ window.removeEventListener('mouseup', onUp);
337
+ }
338
+
339
+ window.addEventListener('mousemove', onMove);
340
+ window.addEventListener('mouseup', onUp);
341
+ }
342
+
149
343
  let popoverStyle = $derived.by(() => {
150
344
  if (!anchorPoint) return '';
151
345
 
@@ -171,24 +365,27 @@
171
365
  return `left: ${left}px; top: ${top}px; max-height: min(600px, ${maxH}px);`;
172
366
  });
173
367
 
174
- function toggleBlock(index: number, x: number, y: number) {
368
+ function toggleBlock(editorIndex: number, x: number, y: number) {
175
369
  editingFrontmatter = false;
176
- if (activeIndex === index) {
370
+ const eb = editorBlocks[editorIndex];
371
+ if (!eb) return;
372
+
373
+ if (activeIndex === editorIndex) {
177
374
  activeIndex = null;
178
375
  anchorPoint = null;
179
376
  pendingRuneIndex = null;
180
377
  } else {
181
378
  editSessionId++;
182
- activeIndex = index;
379
+ activeIndex = editorIndex;
183
380
  anchorPoint = { x, y };
184
381
  pendingRuneIndex = null;
185
382
  }
186
383
  }
187
384
 
188
- function handleRuneClick(index: number, x: number, y: number, nestedRuneIndex?: number) {
385
+ function handleRuneClick(editorIndex: number, x: number, y: number, nestedRuneIndex?: number) {
189
386
  editingFrontmatter = false;
190
387
  editSessionId++;
191
- activeIndex = index;
388
+ activeIndex = editorIndex;
192
389
  anchorPoint = { x, y };
193
390
  pendingRuneIndex = nestedRuneIndex ?? null;
194
391
  }
@@ -207,6 +404,10 @@
207
404
  function handleKeydown(e: KeyboardEvent) {
208
405
  if (readOnly) return;
209
406
  if (e.key === 'Escape') {
407
+ if (proseInlineEdit) {
408
+ closeProseInlineEdit();
409
+ return;
410
+ }
210
411
  if (activeIndex !== null || editingFrontmatter) {
211
412
  activeIndex = null;
212
413
  editingFrontmatter = false;
@@ -231,21 +432,24 @@
231
432
 
232
433
  // ── Block operations ─────────────────────────────────────────
233
434
 
234
- function handleUpdateBlock(index: number, updated: ParsedBlock) {
235
- blocks = blocks.map((b, i) => (i === index ? updated : b));
435
+ /** Update a block by its flat index in the blocks array */
436
+ function handleUpdateBlock(flatIndex: number, updated: ParsedBlock) {
437
+ blocks = blocks.map((b, i) => (i === flatIndex ? updated : b));
236
438
  syncToSource();
237
439
  }
238
440
 
239
- function handleRemoveBlock(index: number) {
441
+ /** Remove a block by its editor block index */
442
+ function handleRemoveEditorBlock(editorIndex: number) {
443
+ const [startFlat, endFlat] = editorBlockToFlatRange(editorIndex);
240
444
  // Adjust activeIndex
241
445
  if (activeIndex !== null) {
242
- if (activeIndex === index) {
446
+ if (activeIndex === editorIndex) {
243
447
  activeIndex = null;
244
- } else if (activeIndex > index) {
448
+ } else if (activeIndex > editorIndex) {
245
449
  activeIndex--;
246
450
  }
247
451
  }
248
- blocks = blocks.filter((_, i) => i !== index);
452
+ blocks = [...blocks.slice(0, startFlat), ...blocks.slice(endFlat + 1)];
249
453
  syncToSource();
250
454
  }
251
455
 
@@ -276,17 +480,32 @@
276
480
  function handleDrop(e: DragEvent, index: number) {
277
481
  e.preventDefault();
278
482
  if (dragIndex !== null && dragIndex !== index) {
279
- const moved = blocks[dragIndex];
280
- const next = blocks.filter((_, i) => i !== dragIndex);
281
- next.splice(index, 0, moved);
282
- blocks = next;
483
+ // Get the flat block ranges for source and destination
484
+ const [srcStart, srcEnd] = editorBlockToFlatRange(dragIndex);
485
+ const movedBlocks = blocks.slice(srcStart, srcEnd + 1);
486
+
487
+ // Remove the source blocks
488
+ const without = [...blocks.slice(0, srcStart), ...blocks.slice(srcEnd + 1)];
489
+
490
+ // Find the insertion point in the filtered array
491
+ let insertAt: number;
492
+ if (index >= editorBlocks.length) {
493
+ insertAt = without.length;
494
+ } else {
495
+ // Recalculate target position in the reduced array
496
+ const [tgtStart] = editorBlockToFlatRange(index);
497
+ // Adjust for removed blocks if source was before target
498
+ insertAt = tgtStart > srcEnd ? tgtStart - movedBlocks.length : tgtStart;
499
+ }
500
+
501
+ without.splice(insertAt, 0, ...movedBlocks);
502
+ blocks = without;
283
503
 
284
504
  // Update activeIndex to follow the active block
285
505
  if (activeIndex !== null) {
286
506
  if (activeIndex === dragIndex) {
287
507
  activeIndex = index > dragIndex ? index - 1 : index;
288
508
  } else {
289
- // Adjust if the move shifts the active block's position
290
509
  let newActive = activeIndex;
291
510
  if (dragIndex < activeIndex && index >= activeIndex) {
292
511
  newActive--;
@@ -410,12 +629,29 @@
410
629
  }
411
630
  }
412
631
 
413
- const pos = insertAtIndex ?? blocks.length;
414
- blocks = [...blocks.slice(0, pos), newBlock, ...blocks.slice(pos)];
632
+ // Convert editor block index to flat block insertion position
633
+ const editorPos = insertAtIndex ?? editorBlocks.length;
634
+ let flatPos: number;
635
+ if (editorPos >= editorBlocks.length) {
636
+ flatPos = blocks.length;
637
+ } else {
638
+ const [start] = editorBlockToFlatRange(editorPos);
639
+ flatPos = start;
640
+ }
641
+
642
+ blocks = [...blocks.slice(0, flatPos), newBlock, ...blocks.slice(flatPos)];
415
643
  insertAtIndex = null;
416
644
  showInsertMenu = false;
417
645
  editingFrontmatter = false;
418
- activeIndex = pos;
646
+ // Set activeIndex for rune blocks; for prose blocks the new element merges into a prose block
647
+ if (type === 'rune') {
648
+ // Find the new block's editor index after re-grouping
649
+ const newEditorBlocks = groupIntoEditorBlocks(blocks);
650
+ activeIndex = newEditorBlocks.findIndex(eb => eb.type === 'rune' && eb === blocks[flatPos]);
651
+ if (activeIndex === -1) activeIndex = null;
652
+ } else {
653
+ activeIndex = null;
654
+ }
419
655
  syncToSource();
420
656
  }
421
657
 
@@ -429,10 +665,15 @@
429
665
  mapping: SectionMapping;
430
666
  } | null = $state(null);
431
667
 
432
- function handleSectionClick(index: number, info: SectionClickInfo) {
433
- const block = blocks[index];
668
+ function handleSectionClick(editorIndex: number, info: SectionClickInfo) {
669
+ const eb = editorBlocks[editorIndex];
670
+ if (!eb || eb.type !== 'rune') return;
671
+ const [flatIndex] = editorBlockToFlatRange(editorIndex);
672
+ const block = blocks[flatIndex];
434
673
  if (block.type !== 'rune') return;
435
674
  const rb = block as RuneBlock;
675
+ // Use flatIndex for all block operations below
676
+ const index = flatIndex;
436
677
 
437
678
  if (info.editType === 'link') {
438
679
  const mapping = findActionMapping(rb.innerContent, info.text, info.href ?? '');
@@ -576,6 +817,16 @@
576
817
  function handleCommandEditChange(newCode: string) {
577
818
  if (!commandEdit) return;
578
819
  const block = blocks[commandEdit.blockIndex];
820
+
821
+ if (block.type === 'fence') {
822
+ // Direct fence block (from prose section)
823
+ const fb = block as FenceBlock;
824
+ const updated: FenceBlock = { ...fb, code: newCode, source: '' };
825
+ updated.source = rebuildFenceSource(updated);
826
+ handleUpdateBlock(commandEdit.blockIndex, updated);
827
+ return;
828
+ }
829
+
579
830
  if (block.type !== 'rune') return;
580
831
  const rb = block as RuneBlock;
581
832
 
@@ -590,6 +841,15 @@
590
841
  if (!commandEdit) return;
591
842
  const blockIndex = commandEdit.blockIndex;
592
843
  const block = blocks[blockIndex];
844
+
845
+ if (block.type === 'fence') {
846
+ // Direct fence block — remove entirely
847
+ commandEdit = null;
848
+ blocks = blocks.filter((_, i) => i !== blockIndex);
849
+ syncToSource();
850
+ return;
851
+ }
852
+
593
853
  if (block.type !== 'rune') return;
594
854
  const rb = block as RuneBlock;
595
855
 
@@ -609,6 +869,27 @@
609
869
  function handleLanguageChange(newLanguage: string) {
610
870
  if (!commandEdit) return;
611
871
  const block = blocks[commandEdit.blockIndex];
872
+
873
+ if (block.type === 'fence') {
874
+ // Direct fence block (from prose section)
875
+ const fb = block as FenceBlock;
876
+ const updated: FenceBlock = { ...fb, language: newLanguage, source: '' };
877
+ updated.source = rebuildFenceSource(updated);
878
+
879
+ // Update the mapping
880
+ const afterDelimiter = commandEdit.mapping.opener.slice(commandEdit.mapping.delimiter.length);
881
+ const infoString = afterDelimiter.replace(/^\w*/, '').trim();
882
+ const newOpener = commandEdit.mapping.delimiter + newLanguage + (infoString ? ' ' + infoString : '');
883
+ const newSource = newOpener + '\n' + commandEdit.mapping.code + '\n' + commandEdit.mapping.delimiter;
884
+ commandEdit = {
885
+ ...commandEdit,
886
+ mapping: { ...commandEdit.mapping, language: newLanguage, opener: newOpener, source: newSource },
887
+ };
888
+
889
+ handleUpdateBlock(commandEdit.blockIndex, updated);
890
+ return;
891
+ }
892
+
612
893
  if (block.type !== 'rune') return;
613
894
  const rb = block as RuneBlock;
614
895
 
@@ -702,9 +983,10 @@
702
983
 
703
984
  function handleFieldEdit(dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) {
704
985
  if (activeIndex === null) return;
986
+ const [flatIndex] = editorBlockToFlatRange(activeIndex);
705
987
  commandEdit = null;
706
988
  inlineEdit = {
707
- blockIndex: activeIndex,
989
+ blockIndex: flatIndex,
708
990
  dataName,
709
991
  inlineSource,
710
992
  rect,
@@ -714,9 +996,10 @@
714
996
 
715
997
  function handleFieldCodeEdit(code: string, language: string, rect: DOMRect, mapping: CommandMapping) {
716
998
  if (activeIndex === null) return;
999
+ const [flatIndex] = editorBlockToFlatRange(activeIndex);
717
1000
  inlineEdit = null;
718
1001
  commandEdit = {
719
- blockIndex: activeIndex,
1002
+ blockIndex: flatIndex,
720
1003
  rect,
721
1004
  mapping,
722
1005
  };
@@ -777,7 +1060,7 @@
777
1060
  </div>
778
1061
  {/if}
779
1062
 
780
- {#each blocks as block, i (block.id)}
1063
+ {#each editorBlocks as eb, i (eb.id)}
781
1064
  <div
782
1065
  class="block-editor__row"
783
1066
  class:hovered={!readOnly && hoveredIndex === i}
@@ -788,23 +1071,43 @@
788
1071
  onmouseleave={() => { if (!readOnly && hoveredIndex === i) hoveredIndex = null; }}
789
1072
  >
790
1073
  <div class="block-editor__block-cell">
791
- <BlockCard
792
- {block}
793
- {themeConfig}
794
- {themeCss}
795
- {highlightCss}
796
- {highlightTransform}
797
- {communityTags}
798
- {communityPostTransforms}
799
- {communityStyles}
800
- {aggregated}
801
- {readOnly}
802
- onsectionclick={readOnly ? undefined : (info) => handleSectionClick(i, info)}
803
- onruneclick={readOnly ? undefined : (info) => handleRuneClick(i, info.x, info.y, info.nestedRuneIndex)}
804
- ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
805
- ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
806
- ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
807
- />
1074
+ {#if eb.type === 'prose'}
1075
+ <ProseBlockCard
1076
+ block={eb}
1077
+ {themeConfig}
1078
+ {themeCss}
1079
+ {highlightCss}
1080
+ {highlightTransform}
1081
+ {communityTags}
1082
+ {communityPostTransforms}
1083
+ {communityStyles}
1084
+ {aggregated}
1085
+ {readOnly}
1086
+ onsectionclick={readOnly ? undefined : (info) => handleProseSectionClick(i, info)}
1087
+ onblockclick={readOnly ? undefined : (info) => toggleBlock(i, info.x, info.y)}
1088
+ ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
1089
+ ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
1090
+ ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
1091
+ />
1092
+ {:else}
1093
+ <BlockCard
1094
+ block={eb}
1095
+ {themeConfig}
1096
+ {themeCss}
1097
+ {highlightCss}
1098
+ {highlightTransform}
1099
+ {communityTags}
1100
+ {communityPostTransforms}
1101
+ {communityStyles}
1102
+ {aggregated}
1103
+ {readOnly}
1104
+ onsectionclick={readOnly ? undefined : (info) => handleSectionClick(i, info)}
1105
+ onruneclick={readOnly ? undefined : (info) => handleRuneClick(i, info.x, info.y, info.nestedRuneIndex)}
1106
+ ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
1107
+ ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
1108
+ ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
1109
+ />
1110
+ {/if}
808
1111
  </div>
809
1112
  {#if !readOnly && showInsertMenuProp}
810
1113
  <!-- Insert markers — top and bottom edges -->
@@ -836,7 +1139,7 @@
836
1139
  onclick={(e) => toggleBlock(i, e.clientX, e.clientY)}
837
1140
  aria-pressed={activeIndex === i}
838
1141
  >
839
- {blockLabel(block)}
1142
+ {blockLabel(eb)}
840
1143
  </button>
841
1144
  {/if}
842
1145
  </div>
@@ -853,26 +1156,39 @@
853
1156
  class="block-editor__popover-backdrop"
854
1157
  onmousedown={() => { activeIndex = null; editingFrontmatter = false; anchorPoint = null; pendingRuneIndex = null; }}
855
1158
  ></div>
856
- <div class="block-editor__popover" style={popoverStyle}>
1159
+ <div class="block-editor__popover" style={popoverStyle} onmousedown={handlePopoverDragStart}>
857
1160
  {#if editingFrontmatter}
858
1161
  <FrontmatterEditPanel
859
1162
  onclose={() => { editingFrontmatter = false; anchorPoint = null; }}
860
1163
  />
861
- {:else if activeIndex !== null && blocks[activeIndex]}
1164
+ {:else if activeIndex !== null && editorBlocks[activeIndex]?.type === 'rune'}
1165
+ {@const activeBlock = blocks[editorBlockToFlatRange(activeIndex)[0]]}
862
1166
  {#key editSessionId}
863
1167
  <BlockEditPanel
864
- block={blocks[activeIndex]}
1168
+ block={activeBlock}
865
1169
  {runeMap}
866
1170
  runes={() => runes}
867
1171
  {aggregated}
868
1172
  initialRuneIndex={pendingRuneIndex}
869
- onupdate={(updated) => handleUpdateBlock(activeIndex!, updated)}
870
- onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null; handleRemoveBlock(idx); }}
1173
+ onupdate={(updated) => handleUpdateBlock(editorBlockToFlatRange(activeIndex!)[0], updated)}
1174
+ onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null; handleRemoveEditorBlock(idx); }}
871
1175
  onclose={() => { activeIndex = null; anchorPoint = null; pendingRuneIndex = null; }}
872
1176
  oneditfield={handleFieldEdit}
873
1177
  oneditcode={handleFieldCodeEdit}
874
1178
  />
875
1179
  {/key}
1180
+ {:else if activeIndex !== null && editorBlocks[activeIndex]?.type === 'prose'}
1181
+ {@const proseBlock = editorBlocks[activeIndex] as ProseBlock}
1182
+ {#key editSessionId}
1183
+ <ProseEditPanel
1184
+ block={proseBlock}
1185
+ runes={() => runes}
1186
+ {aggregated}
1187
+ onupdate={(updated) => handleUpdateProseBlock(activeIndex!, updated)}
1188
+ onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; handleRemoveEditorBlock(idx); }}
1189
+ onclose={() => { activeIndex = null; anchorPoint = null; }}
1190
+ />
1191
+ {/key}
876
1192
  {/if}
877
1193
  </div>
878
1194
  {/if}
@@ -937,6 +1253,16 @@
937
1253
  onclose={closeIconEdit}
938
1254
  />
939
1255
  {/if}
1256
+
1257
+ {#if proseInlineEdit}
1258
+ <InlineEditPopover
1259
+ anchorRect={proseInlineEdit.rect}
1260
+ dataName={proseInlineEdit.mapping.dataName}
1261
+ inlineSource={proseInlineEdit.inlineSource}
1262
+ onchange={handleProseInlineEditChange}
1263
+ onclose={closeProseInlineEdit}
1264
+ />
1265
+ {/if}
940
1266
  </div>
941
1267
 
942
1268
 
@@ -1167,6 +1493,14 @@
1167
1493
  animation: popover-enter 0.15s ease-out;
1168
1494
  }
1169
1495
 
1496
+ .block-editor__popover :global(.edit-panel__header) {
1497
+ cursor: grab;
1498
+ }
1499
+
1500
+ .block-editor__popover :global(.edit-panel__header):active {
1501
+ cursor: grabbing;
1502
+ }
1503
+
1170
1504
  @keyframes popover-enter {
1171
1505
  from {
1172
1506
  opacity: 0;
@@ -26,6 +26,7 @@
26
26
 
27
27
  let container: HTMLElement;
28
28
  let view = $state<EditorView | undefined>(undefined);
29
+ let lastEmitted = '';
29
30
 
30
31
  const langCompartment = new Compartment();
31
32
  const markdocCompartment = new Compartment();
@@ -145,7 +146,8 @@
145
146
  ]),
146
147
  EditorView.updateListener.of((update) => {
147
148
  if (update.docChanged) {
148
- onchange(update.state.doc.toString());
149
+ lastEmitted = update.state.doc.toString();
150
+ onchange(lastEmitted);
149
151
  }
150
152
  }),
151
153
  EditorView.lineWrapping,
@@ -214,11 +216,19 @@
214
216
  if (!editor) return;
215
217
 
216
218
  const cmContent = editor.state.doc.toString();
217
- if (current !== cmContent) {
218
- editor.dispatch({
219
- changes: { from: 0, to: editor.state.doc.length, insert: current },
220
- });
219
+ if (current === cmContent) return;
220
+
221
+ // If CM still has what we last emitted, the incoming content
222
+ // is our own edit coming back (possibly normalized) — skip sync
223
+ if (lastEmitted && cmContent === lastEmitted) {
224
+ lastEmitted = '';
225
+ return;
221
226
  }
227
+
228
+ lastEmitted = '';
229
+ editor.dispatch({
230
+ changes: { from: 0, to: editor.state.doc.length, insert: current },
231
+ });
222
232
  });
223
233
  </script>
224
234