@refrakt-md/editor 0.8.0 → 0.8.2

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 (42) hide show
  1. package/app/dist/assets/{index-Ca-wW6uw.js → index-80NtMar1.js} +1 -1
  2. package/app/dist/assets/index-B6H6LF1M.css +1 -0
  3. package/app/dist/assets/{index-Dg4A5Pez.js → index-BDj1XPol.js} +1 -1
  4. package/app/dist/assets/{index-BfYWp0QC.js → index-BXe1fKaT.js} +1 -1
  5. package/app/dist/assets/{index-Cq0Maciq.js → index-BfxTGrHB.js} +1 -1
  6. package/app/dist/assets/{index-BsSUa0GD.js → index-Bn8ajfVl.js} +1 -1
  7. package/app/dist/assets/{index-D6vnTt4b.js → index-CCkzIGTi.js} +2 -2
  8. package/app/dist/assets/{index-BehCztSl.js → index-CXeK-dZx.js} +1 -1
  9. package/app/dist/assets/{index-iGDqoXj_.js → index-CaRBCHaX.js} +1 -1
  10. package/app/dist/assets/index-Cd12jZId.js +479 -0
  11. package/app/dist/assets/{index-D5pMhPrg.js → index-Cgbvx23V.js} +1 -1
  12. package/app/dist/assets/{index-IU6QYZAa.js → index-D5ucdUTo.js} +1 -1
  13. package/app/dist/assets/{index-CdpS6tGk.js → index-DGYxLhpR.js} +1 -1
  14. package/app/dist/assets/{index-RKEq45V5.js → index-DNJBunzP.js} +1 -1
  15. package/app/dist/assets/{index-Cgaw2jCE.js → index-DNtuldOx.js} +1 -1
  16. package/app/dist/assets/{index-BEPqnnsd.js → index-DQUOY-pF.js} +1 -1
  17. package/app/dist/assets/{index-2hOoPFOR.js → index-DskvyNKT.js} +1 -1
  18. package/app/dist/assets/{index-CLZfwYyS.js → index-aPeHMqUX.js} +1 -1
  19. package/app/dist/assets/{index-BobjskUl.js → index-dGztG-54.js} +1 -1
  20. package/app/dist/assets/{index-DHALjxX5.js → index-xo7v6nRB.js} +1 -1
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/api/client.ts +81 -0
  23. package/app/src/lib/components/ActionEditPopover.svelte +267 -0
  24. package/app/src/lib/components/BlockCard.svelte +285 -0
  25. package/app/src/lib/components/BlockEditPanel.svelte +640 -260
  26. package/app/src/lib/components/BlockEditor.svelte +513 -52
  27. package/app/src/lib/components/CodeEditPopover.svelte +444 -0
  28. package/app/src/lib/components/ContentModelTree.svelte +835 -0
  29. package/app/src/lib/components/EditorLayout.svelte +1 -6
  30. package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
  31. package/app/src/lib/components/IconPickerPopover.svelte +389 -0
  32. package/app/src/lib/components/ImageEditPopover.svelte +519 -0
  33. package/app/src/lib/components/InlineEditPopover.svelte +616 -0
  34. package/app/src/lib/components/RuneAttributes.svelte +51 -0
  35. package/app/src/lib/editor/block-parser.ts +424 -6
  36. package/dist/server.d.ts +1 -0
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +189 -2
  39. package/dist/server.js.map +1 -1
  40. package/package.json +6 -6
  41. package/app/dist/assets/index-98ylvoBO.css +0 -1
  42. package/app/dist/assets/index-CVzOx0nV.js +0 -372
@@ -8,13 +8,23 @@
8
8
  buildRuneMap,
9
9
  blockLabel,
10
10
  extractRuneInner,
11
+ rebuildRuneSource,
11
12
  type ParsedBlock,
13
+ type RuneBlock,
12
14
  } from '../editor/block-parser.js';
15
+ 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
+ import { stripInlineMarkdown } from '../editor/inline-markdown.js';
13
17
  import { editorState } from '../state/editor.svelte.js';
14
18
  import BlockCard from './BlockCard.svelte';
19
+ import type { SectionClickInfo } from './BlockCard.svelte';
15
20
  import BlockEditPanel from './BlockEditPanel.svelte';
16
21
  import FrontmatterEditPanel from './FrontmatterEditPanel.svelte';
17
22
  import InsertBlockDialog from './InsertBlockDialog.svelte';
23
+ import InlineEditPopover from './InlineEditPopover.svelte';
24
+ import ActionEditPopover from './ActionEditPopover.svelte';
25
+ import CodeEditPopover from './CodeEditPopover.svelte';
26
+ import ImageEditPopover from './ImageEditPopover.svelte';
27
+ import IconPickerPopover from './IconPickerPopover.svelte';
18
28
 
19
29
  interface Props {
20
30
  bodyContent: string;
@@ -99,17 +109,14 @@
99
109
  reconcileIds(blocks, newBlocks);
100
110
  blocks = newBlocks;
101
111
  lastParsedSource = body;
102
- // Close edit panel when switching files
112
+ // Close edit panel and inline popover when switching files
103
113
  activeIndex = null;
114
+ editingFrontmatter = false;
115
+ anchorPoint = null;
116
+ inlineEdit = null;
104
117
  }
105
118
  });
106
119
 
107
- // Sync edit panel open state to global state (for layout adjustments)
108
- $effect(() => {
109
- editorState.editPanelOpen = !readOnly && (activeIndex !== null || editingFrontmatter);
110
- return () => { editorState.editPanelOpen = false; };
111
- });
112
-
113
120
  /** Sync blocks back to source text */
114
121
  function syncToSource() {
115
122
  const newSource = serializeBlocks(blocks);
@@ -130,15 +137,71 @@
130
137
 
131
138
  let activeIndex: number | null = $state(null);
132
139
  let hoveredIndex: number | null = $state(null);
140
+ let anchorPoint: { x: number; y: number } | null = $state(null);
141
+ let pendingRuneIndex: number | null = $state(null);
142
+ let editSessionId: number = $state(0);
143
+
144
+ // ── Popover positioning ─────────────────────────────────────
145
+
146
+ const POPOVER_WIDTH = 420;
147
+ const POPOVER_GAP = 12;
148
+
149
+ let popoverStyle = $derived.by(() => {
150
+ if (!anchorPoint) return '';
151
+
152
+ const vw = window.innerWidth;
153
+ const vh = window.innerHeight;
154
+
155
+ // Prefer placing to the right of the click point
156
+ let left = anchorPoint.x + POPOVER_GAP;
157
+ if (left + POPOVER_WIDTH > vw - 16) {
158
+ left = anchorPoint.x - POPOVER_WIDTH - POPOVER_GAP;
159
+ }
160
+ if (left < 16) {
161
+ left = vw - POPOVER_WIDTH - 16;
162
+ }
163
+
164
+ // Vertical: start at click Y, clamped to viewport
165
+ let top = anchorPoint.y;
166
+ const maxH = vh - 120;
167
+ const maxTop = vh - Math.min(600, maxH) - 16;
168
+ if (top > maxTop) top = maxTop;
169
+ if (top < 60) top = 60;
170
+
171
+ return `left: ${left}px; top: ${top}px; max-height: min(600px, ${maxH}px);`;
172
+ });
173
+
174
+ function toggleBlock(index: number, x: number, y: number) {
175
+ editingFrontmatter = false;
176
+ if (activeIndex === index) {
177
+ activeIndex = null;
178
+ anchorPoint = null;
179
+ pendingRuneIndex = null;
180
+ } else {
181
+ editSessionId++;
182
+ activeIndex = index;
183
+ anchorPoint = { x, y };
184
+ pendingRuneIndex = null;
185
+ }
186
+ }
133
187
 
134
- function toggleBlock(index: number) {
188
+ function handleRuneClick(index: number, x: number, y: number, nestedRuneIndex?: number) {
135
189
  editingFrontmatter = false;
136
- activeIndex = activeIndex === index ? null : index;
190
+ editSessionId++;
191
+ activeIndex = index;
192
+ anchorPoint = { x, y };
193
+ pendingRuneIndex = nestedRuneIndex ?? null;
137
194
  }
138
195
 
139
- function toggleFrontmatter() {
196
+ function toggleFrontmatter(e: MouseEvent) {
140
197
  activeIndex = null;
141
- editingFrontmatter = !editingFrontmatter;
198
+ if (editingFrontmatter) {
199
+ editingFrontmatter = false;
200
+ anchorPoint = null;
201
+ } else {
202
+ editingFrontmatter = true;
203
+ anchorPoint = { x: e.clientX, y: e.clientY };
204
+ }
142
205
  }
143
206
 
144
207
  function handleKeydown(e: KeyboardEvent) {
@@ -147,10 +210,25 @@
147
210
  if (activeIndex !== null || editingFrontmatter) {
148
211
  activeIndex = null;
149
212
  editingFrontmatter = false;
213
+ anchorPoint = null;
214
+ pendingRuneIndex = null;
150
215
  }
151
216
  }
152
217
  }
153
218
 
219
+ function handleListScroll() {
220
+ if (activeIndex !== null || editingFrontmatter) {
221
+ activeIndex = null;
222
+ editingFrontmatter = false;
223
+ anchorPoint = null;
224
+ pendingRuneIndex = null;
225
+ }
226
+ }
227
+
228
+ function handleResize() {
229
+ if (anchorPoint) anchorPoint = { ...anchorPoint };
230
+ }
231
+
154
232
  // ── Block operations ─────────────────────────────────────────
155
233
 
156
234
  function handleUpdateBlock(index: number, updated: ParsedBlock) {
@@ -178,6 +256,9 @@
178
256
 
179
257
  function handleDragStart(e: DragEvent, index: number) {
180
258
  dragIndex = index;
259
+ activeIndex = null;
260
+ editingFrontmatter = false;
261
+ anchorPoint = null;
181
262
  if (e.dataTransfer) {
182
263
  e.dataTransfer.effectAllowed = 'move';
183
264
  e.dataTransfer.setData('text/plain', String(index));
@@ -338,6 +419,309 @@
338
419
  syncToSource();
339
420
  }
340
421
 
422
+ // ── Inline section editing ──────────────────────────────────
423
+
424
+ let inlineEdit: {
425
+ blockIndex: number;
426
+ dataName: string;
427
+ inlineSource: string;
428
+ rect: DOMRect;
429
+ mapping: SectionMapping;
430
+ } | null = $state(null);
431
+
432
+ function handleSectionClick(index: number, info: SectionClickInfo) {
433
+ const block = blocks[index];
434
+ if (block.type !== 'rune') return;
435
+ const rb = block as RuneBlock;
436
+
437
+ if (info.editType === 'link') {
438
+ const mapping = findActionMapping(rb.innerContent, info.text, info.href ?? '');
439
+ if (!mapping) return;
440
+
441
+ actionEdit = {
442
+ blockIndex: index,
443
+ rect: info.rect,
444
+ mapping,
445
+ };
446
+ return;
447
+ }
448
+
449
+ if (info.editType === 'code') {
450
+ const mapping = findCommandMapping(rb.innerContent, info.text);
451
+ if (!mapping) return;
452
+
453
+ commandEdit = {
454
+ blockIndex: index,
455
+ rect: info.rect,
456
+ mapping,
457
+ };
458
+ return;
459
+ }
460
+
461
+ if (info.editType === 'image') {
462
+ const imgSrc = info.href ?? '';
463
+ const mapping = findImageMapping(rb.innerContent, imgSrc);
464
+ if (!mapping) return;
465
+
466
+ imageEdit = {
467
+ blockIndex: index,
468
+ mapping,
469
+ };
470
+ return;
471
+ }
472
+
473
+ if (info.editType === 'icon') {
474
+ const iconName = info.iconName ?? '';
475
+ const mapping = findIconMapping(rb.innerContent, iconName);
476
+ if (!mapping) return;
477
+
478
+ iconEdit = {
479
+ blockIndex: index,
480
+ mapping,
481
+ };
482
+ return;
483
+ }
484
+
485
+ // Default: inline text editing
486
+ const mapping = findSectionMapping(rb.innerContent, info.dataName, info.text);
487
+ if (!mapping) return;
488
+
489
+ inlineEdit = {
490
+ blockIndex: index,
491
+ dataName: info.dataName,
492
+ inlineSource: mapping.inlineSource,
493
+ rect: info.rect,
494
+ mapping,
495
+ };
496
+ }
497
+
498
+ function handleInlineEditChange(newInlineSource: string) {
499
+ if (!inlineEdit) return;
500
+ const block = blocks[inlineEdit.blockIndex];
501
+ if (block.type !== 'rune') return;
502
+ const rb = block as RuneBlock;
503
+
504
+ const newInner = applySectionEdit(rb.innerContent, inlineEdit.mapping, newInlineSource);
505
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
506
+ updated.source = rebuildRuneSource(updated);
507
+
508
+ // Update the mapping to reflect the new source so subsequent edits work
509
+ inlineEdit = {
510
+ ...inlineEdit,
511
+ inlineSource: newInlineSource,
512
+ mapping: {
513
+ ...inlineEdit.mapping,
514
+ text: stripInlineMarkdown(newInlineSource),
515
+ source: inlineEdit.mapping.sourcePrefix + newInlineSource,
516
+ inlineSource: newInlineSource,
517
+ },
518
+ };
519
+
520
+ handleUpdateBlock(inlineEdit.blockIndex, updated);
521
+ }
522
+
523
+ function closeInlineEdit() {
524
+ inlineEdit = null;
525
+ }
526
+
527
+ // ── Action item editing ────────────────────────────────────
528
+
529
+ let actionEdit: {
530
+ blockIndex: number;
531
+ rect: DOMRect;
532
+ mapping: ActionMapping;
533
+ } | null = $state(null);
534
+
535
+ function handleActionEditChange(newText: string, newHref: string) {
536
+ if (!actionEdit) return;
537
+ const block = blocks[actionEdit.blockIndex];
538
+ if (block.type !== 'rune') return;
539
+ const rb = block as RuneBlock;
540
+
541
+ const newInner = applyActionEdit(rb.innerContent, actionEdit.mapping, newText, newHref);
542
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
543
+ updated.source = rebuildRuneSource(updated);
544
+
545
+ handleUpdateBlock(actionEdit.blockIndex, updated);
546
+ }
547
+
548
+ function handleActionRemove() {
549
+ if (!actionEdit) return;
550
+ const blockIndex = actionEdit.blockIndex;
551
+ const block = blocks[blockIndex];
552
+ if (block.type !== 'rune') return;
553
+ const rb = block as RuneBlock;
554
+
555
+ // Remove the entire list item line from the inner content
556
+ const newInner = rb.innerContent.replace(actionEdit.mapping.source + '\n', '').replace(actionEdit.mapping.source, '');
557
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
558
+ updated.source = rebuildRuneSource(updated);
559
+
560
+ actionEdit = null;
561
+ handleUpdateBlock(blockIndex, updated);
562
+ }
563
+
564
+ function closeActionEdit() {
565
+ actionEdit = null;
566
+ }
567
+
568
+ // ── Command (code block) editing ──────────────────────────
569
+
570
+ let commandEdit: {
571
+ blockIndex: number;
572
+ rect: DOMRect;
573
+ mapping: CommandMapping;
574
+ } | null = $state(null);
575
+
576
+ function handleCommandEditChange(newCode: string) {
577
+ if (!commandEdit) return;
578
+ const block = blocks[commandEdit.blockIndex];
579
+ if (block.type !== 'rune') return;
580
+ const rb = block as RuneBlock;
581
+
582
+ const newInner = applyCommandEdit(rb.innerContent, commandEdit.mapping, newCode);
583
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
584
+ updated.source = rebuildRuneSource(updated);
585
+
586
+ handleUpdateBlock(commandEdit.blockIndex, updated);
587
+ }
588
+
589
+ function handleCommandRemove() {
590
+ if (!commandEdit) return;
591
+ const blockIndex = commandEdit.blockIndex;
592
+ const block = blocks[blockIndex];
593
+ if (block.type !== 'rune') return;
594
+ const rb = block as RuneBlock;
595
+
596
+ // Remove the entire fenced code block from the inner content
597
+ const newInner = rb.innerContent.replace(commandEdit.mapping.source + '\n', '').replace(commandEdit.mapping.source, '');
598
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
599
+ updated.source = rebuildRuneSource(updated);
600
+
601
+ commandEdit = null;
602
+ handleUpdateBlock(blockIndex, updated);
603
+ }
604
+
605
+ function closeCommandEdit() {
606
+ commandEdit = null;
607
+ }
608
+
609
+ function handleLanguageChange(newLanguage: string) {
610
+ if (!commandEdit) return;
611
+ const block = blocks[commandEdit.blockIndex];
612
+ if (block.type !== 'rune') return;
613
+ const rb = block as RuneBlock;
614
+
615
+ const newInner = applyLanguageEdit(rb.innerContent, commandEdit.mapping, newLanguage);
616
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
617
+ updated.source = rebuildRuneSource(updated);
618
+
619
+ // Update the mapping to reflect the new language and opener
620
+ const afterDelimiter = commandEdit.mapping.opener.slice(commandEdit.mapping.delimiter.length);
621
+ const infoString = afterDelimiter.replace(/^\w*/, '').trim();
622
+ const newOpener = commandEdit.mapping.delimiter + newLanguage + (infoString ? ' ' + infoString : '');
623
+ const newSource = newOpener + '\n' + commandEdit.mapping.code + '\n' + commandEdit.mapping.delimiter;
624
+
625
+ commandEdit = {
626
+ ...commandEdit,
627
+ mapping: {
628
+ ...commandEdit.mapping,
629
+ language: newLanguage,
630
+ opener: newOpener,
631
+ source: newSource,
632
+ },
633
+ };
634
+
635
+ handleUpdateBlock(commandEdit.blockIndex, updated);
636
+ }
637
+
638
+ // ── Image editing ────────────────────────────────────────────
639
+
640
+ let imageEdit: {
641
+ blockIndex: number;
642
+ mapping: ImageMapping;
643
+ } | null = $state(null);
644
+
645
+ function handleImageEditChange(newSrc: string, newAlt: string) {
646
+ if (!imageEdit) return;
647
+ const block = blocks[imageEdit.blockIndex];
648
+ if (block.type !== 'rune') return;
649
+ const rb = block as RuneBlock;
650
+
651
+ const newInner = applyImageEdit(rb.innerContent, imageEdit.mapping, newAlt, newSrc);
652
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
653
+ updated.source = rebuildRuneSource(updated);
654
+
655
+ handleUpdateBlock(imageEdit.blockIndex, updated);
656
+ }
657
+
658
+ function handleImageRemove() {
659
+ if (!imageEdit) return;
660
+ const blockIndex = imageEdit.blockIndex;
661
+ const block = blocks[blockIndex];
662
+ if (block.type !== 'rune') return;
663
+ const rb = block as RuneBlock;
664
+
665
+ const newInner = rb.innerContent.replace(imageEdit.mapping.source + '\n', '').replace(imageEdit.mapping.source, '');
666
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
667
+ updated.source = rebuildRuneSource(updated);
668
+
669
+ imageEdit = null;
670
+ handleUpdateBlock(blockIndex, updated);
671
+ }
672
+
673
+ function closeImageEdit() {
674
+ imageEdit = null;
675
+ }
676
+
677
+ // ── Icon editing ────────────────────────────────────────────
678
+
679
+ let iconEdit: {
680
+ blockIndex: number;
681
+ mapping: IconMapping;
682
+ } | null = $state(null);
683
+
684
+ function handleIconEditChange(newIconName: string) {
685
+ if (!iconEdit) return;
686
+ const block = blocks[iconEdit.blockIndex];
687
+ if (block.type !== 'rune') return;
688
+ const rb = block as RuneBlock;
689
+
690
+ const newInner = applyIconEdit(rb.innerContent, iconEdit.mapping, newIconName);
691
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
692
+ updated.source = rebuildRuneSource(updated);
693
+
694
+ handleUpdateBlock(iconEdit.blockIndex, updated);
695
+ }
696
+
697
+ function closeIconEdit() {
698
+ iconEdit = null;
699
+ }
700
+
701
+ // ── Field edit from Structure tab ──────────────────────────────
702
+
703
+ function handleFieldEdit(dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) {
704
+ if (activeIndex === null) return;
705
+ commandEdit = null;
706
+ inlineEdit = {
707
+ blockIndex: activeIndex,
708
+ dataName,
709
+ inlineSource,
710
+ rect,
711
+ mapping,
712
+ };
713
+ }
714
+
715
+ function handleFieldCodeEdit(code: string, language: string, rect: DOMRect, mapping: CommandMapping) {
716
+ if (activeIndex === null) return;
717
+ inlineEdit = null;
718
+ commandEdit = {
719
+ blockIndex: activeIndex,
720
+ rect,
721
+ mapping,
722
+ };
723
+ }
724
+
341
725
  // Group runes by category for the insert menu
342
726
  let runesByCategory = $derived.by(() => {
343
727
  const map = new Map<string, RuneInfo[]>();
@@ -350,7 +734,7 @@
350
734
  });
351
735
  </script>
352
736
 
353
- <svelte:window onkeydown={handleKeydown} />
737
+ <svelte:window onkeydown={handleKeydown} onresize={handleResize} />
354
738
 
355
739
  <div class="block-editor">
356
740
  {#if blocks.length === 0 && !readOnly}
@@ -366,10 +750,10 @@
366
750
  </div>
367
751
  {/if}
368
752
 
369
- <div class="block-editor__stage" class:editing={!readOnly && (activeIndex !== null || editingFrontmatter)}>
753
+ <div class="block-editor__stage">
370
754
  <!-- Scrollable block list -->
371
755
  <div class="block-editor__scroll">
372
- <div class="block-editor__list-wrap">
756
+ <div class="block-editor__list-wrap" onscroll={handleListScroll}>
373
757
  <!-- Frontmatter summary header (blocks mode only) -->
374
758
  {#if !readOnly}
375
759
  <div class="block-editor__fm-header">
@@ -382,7 +766,7 @@
382
766
  <button
383
767
  class="block-editor__fm-edit"
384
768
  class:active={editingFrontmatter}
385
- onclick={toggleFrontmatter}
769
+ onclick={(e) => toggleFrontmatter(e)}
386
770
  title="Edit frontmatter"
387
771
  >
388
772
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
@@ -414,6 +798,9 @@
414
798
  {communityPostTransforms}
415
799
  {communityStyles}
416
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)}
417
804
  ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
418
805
  ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
419
806
  ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
@@ -446,7 +833,7 @@
446
833
  <!-- Block label — slides in from right on hover, pinned when active -->
447
834
  <button
448
835
  class="block-editor__hover-label"
449
- onclick={() => toggleBlock(i)}
836
+ onclick={(e) => toggleBlock(i, e.clientX, e.clientY)}
450
837
  aria-pressed={activeIndex === i}
451
838
  >
452
839
  {blockLabel(block)}
@@ -457,30 +844,39 @@
457
844
  </div>
458
845
  </div>
459
846
 
460
- <!-- Edit panel — slides in from the right (blocks mode only) -->
461
- {#if !readOnly}
462
- <div class="block-editor__edit-panel">
463
- {#if editingFrontmatter}
464
- <FrontmatterEditPanel
465
- onclose={() => { editingFrontmatter = false; }}
466
- />
467
- {:else if activeIndex !== null && blocks[activeIndex]}
468
- {#key activeIndex}
469
- <BlockEditPanel
470
- block={blocks[activeIndex]}
471
- {runeMap}
472
- runes={() => runes}
473
- {aggregated}
474
- onupdate={(updated) => handleUpdateBlock(activeIndex!, updated)}
475
- onremove={() => { const idx = activeIndex!; activeIndex = null; handleRemoveBlock(idx); }}
476
- onclose={() => { activeIndex = null; }}
477
- />
478
- {/key}
479
- {/if}
480
- </div>
481
- {/if}
482
847
  </div>
483
848
 
849
+ <!-- Popover edit panel — anchored to click position -->
850
+ {#if !readOnly && (activeIndex !== null || editingFrontmatter)}
851
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
852
+ <div
853
+ class="block-editor__popover-backdrop"
854
+ onmousedown={() => { activeIndex = null; editingFrontmatter = false; anchorPoint = null; pendingRuneIndex = null; }}
855
+ ></div>
856
+ <div class="block-editor__popover" style={popoverStyle}>
857
+ {#if editingFrontmatter}
858
+ <FrontmatterEditPanel
859
+ onclose={() => { editingFrontmatter = false; anchorPoint = null; }}
860
+ />
861
+ {:else if activeIndex !== null && blocks[activeIndex]}
862
+ {#key editSessionId}
863
+ <BlockEditPanel
864
+ block={blocks[activeIndex]}
865
+ {runeMap}
866
+ runes={() => runes}
867
+ {aggregated}
868
+ initialRuneIndex={pendingRuneIndex}
869
+ onupdate={(updated) => handleUpdateBlock(activeIndex!, updated)}
870
+ onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null; handleRemoveBlock(idx); }}
871
+ onclose={() => { activeIndex = null; anchorPoint = null; pendingRuneIndex = null; }}
872
+ oneditfield={handleFieldEdit}
873
+ oneditcode={handleFieldCodeEdit}
874
+ />
875
+ {/key}
876
+ {/if}
877
+ </div>
878
+ {/if}
879
+
484
880
  {#if showInsertMenu}
485
881
  <InsertBlockDialog
486
882
  {runes}
@@ -489,6 +885,58 @@
489
885
  onclose={closeInsertMenu}
490
886
  />
491
887
  {/if}
888
+
889
+ {#if inlineEdit}
890
+ <InlineEditPopover
891
+ anchorRect={inlineEdit.rect}
892
+ dataName={inlineEdit.dataName}
893
+ inlineSource={inlineEdit.inlineSource}
894
+ onchange={handleInlineEditChange}
895
+ onclose={closeInlineEdit}
896
+ />
897
+ {/if}
898
+
899
+ {#if actionEdit}
900
+ <ActionEditPopover
901
+ anchorRect={actionEdit.rect}
902
+ text={actionEdit.mapping.text}
903
+ href={actionEdit.mapping.href}
904
+ onchange={handleActionEditChange}
905
+ onremove={handleActionRemove}
906
+ onclose={closeActionEdit}
907
+ />
908
+ {/if}
909
+
910
+ {#if commandEdit}
911
+ <CodeEditPopover
912
+ anchorRect={commandEdit.rect}
913
+ code={commandEdit.mapping.code}
914
+ language={commandEdit.mapping.language}
915
+ onchange={handleCommandEditChange}
916
+ onlanguagechange={handleLanguageChange}
917
+ onremove={handleCommandRemove}
918
+ onclose={closeCommandEdit}
919
+ />
920
+ {/if}
921
+
922
+ {#if imageEdit}
923
+ <ImageEditPopover
924
+ currentSrc={imageEdit.mapping.src}
925
+ currentAlt={imageEdit.mapping.alt}
926
+ onchange={(src, alt) => { handleImageEditChange(src, alt); closeImageEdit(); }}
927
+ onremove={handleImageRemove}
928
+ onclose={closeImageEdit}
929
+ />
930
+ {/if}
931
+
932
+ {#if iconEdit}
933
+ <IconPickerPopover
934
+ icons={themeConfig?.icons ?? {}}
935
+ currentIcon={iconEdit.mapping.name}
936
+ onchange={(name) => { handleIconEditChange(name); closeIconEdit(); }}
937
+ onclose={closeIconEdit}
938
+ />
939
+ {/if}
492
940
  </div>
493
941
 
494
942
 
@@ -515,7 +963,6 @@
515
963
  min-height: 0;
516
964
  display: flex;
517
965
  flex-direction: column;
518
- transition: margin-right var(--ed-transition-slow);
519
966
  }
520
967
 
521
968
  .block-editor__list-wrap {
@@ -697,24 +1144,38 @@
697
1144
  color: white;
698
1145
  }
699
1146
 
700
- /* Edit panelfixed to right edge of viewport, outside the card */
701
- .block-editor__edit-panel {
1147
+ /* Popover backdroptransparent click target */
1148
+ .block-editor__popover-backdrop {
702
1149
  position: fixed;
703
- top: calc(60px + var(--ed-space-4));
704
- right: 0;
705
- bottom: 0;
706
- width: 480px;
707
- overflow-y: auto;
1150
+ inset: 0;
1151
+ z-index: 9;
1152
+ }
1153
+
1154
+ /* Popover container — anchored to block card */
1155
+ .block-editor__popover {
1156
+ position: fixed;
1157
+ width: 420px;
1158
+ overflow: hidden;
1159
+ display: flex;
1160
+ flex-direction: column;
708
1161
  background: var(--ed-surface-0);
709
- border-radius: var(--ed-radius-lg) 0 0 0;
710
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
711
- transform: translateX(100%);
712
- transition: transform var(--ed-transition-slow);
1162
+ border-radius: var(--ed-radius-lg);
1163
+ border: 1px solid var(--ed-border-default);
1164
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
1165
+ 0 8px 10px -6px rgba(0, 0, 0, 0.1);
713
1166
  z-index: 10;
1167
+ animation: popover-enter 0.15s ease-out;
714
1168
  }
715
1169
 
716
- .block-editor__stage.editing .block-editor__edit-panel {
717
- transform: translateX(0);
1170
+ @keyframes popover-enter {
1171
+ from {
1172
+ opacity: 0;
1173
+ transform: translateY(4px) scale(0.98);
1174
+ }
1175
+ to {
1176
+ opacity: 1;
1177
+ transform: translateY(0) scale(1);
1178
+ }
718
1179
  }
719
1180
 
720
1181
  /* Empty state */