@refrakt-md/editor 0.7.2 → 0.8.1

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 (50) hide show
  1. package/app/dist/assets/{index-4SP4_AaD.js → index-BBinZAiy.js} +1 -1
  2. package/app/dist/assets/index-BD2EBUrQ.css +1 -0
  3. package/app/dist/assets/{index-D77rckeh.js → index-BLuaHLN3.js} +1 -1
  4. package/app/dist/assets/{index-30gAspk8.js → index-BgCNqcSo.js} +1 -1
  5. package/app/dist/assets/index-BlAOhWAQ.js +453 -0
  6. package/app/dist/assets/{index-BZ4adnS0.js → index-BwFn9q4x.js} +1 -1
  7. package/app/dist/assets/{index-DFkteo0w.js → index-C72UC2ga.js} +1 -1
  8. package/app/dist/assets/{index-x67KGOIr.js → index-COIPZ34u.js} +1 -1
  9. package/app/dist/assets/{index-BEFUVB_B.js → index-CW02bulk.js} +1 -1
  10. package/app/dist/assets/{index-CI5PewQM.js → index-CXFMPmtf.js} +1 -1
  11. package/app/dist/assets/{index-ByHhigzw.js → index-CeU_s7BB.js} +1 -1
  12. package/app/dist/assets/{index-DvgOtlru.js → index-CqHjo2YT.js} +1 -1
  13. package/app/dist/assets/{index-DKnhR16N.js → index-D3TQo8gu.js} +1 -1
  14. package/app/dist/assets/{index-Baf7ZSct.js → index-DVM3uoxc.js} +1 -1
  15. package/app/dist/assets/{index-C9w1RpYY.js → index-DW2zI-Ss.js} +1 -1
  16. package/app/dist/assets/{index--rGC9bba.js → index-D_Y6J00B.js} +1 -1
  17. package/app/dist/assets/{index-kPhFxtn-.js → index-DgIg-QAA.js} +2 -2
  18. package/app/dist/assets/{index-DIuFNfTc.js → index-DmY6uqAw.js} +1 -1
  19. package/app/dist/assets/{index-D1WOi3EN.js → index-DzHt8ZRh.js} +1 -1
  20. package/app/dist/assets/{index-BwWzfQVn.js → index-ZLvRNfLb.js} +1 -1
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/api/client.ts +49 -0
  23. package/app/src/lib/components/ActionEditPopover.svelte +245 -0
  24. package/app/src/lib/components/BlockCard.svelte +255 -1
  25. package/app/src/lib/components/BlockEditPanel.svelte +697 -138
  26. package/app/src/lib/components/BlockEditor.svelte +467 -389
  27. package/app/src/lib/components/CodeEditPopover.svelte +226 -0
  28. package/app/src/lib/components/ContentModelTree.svelte +562 -0
  29. package/app/src/lib/components/ContentTree.svelte +181 -0
  30. package/app/src/lib/components/EditorLayout.svelte +1 -6
  31. package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
  32. package/app/src/lib/components/HeaderBar.svelte +38 -0
  33. package/app/src/lib/components/InlineEditPopover.svelte +593 -0
  34. package/app/src/lib/components/InsertBlockDialog.svelte +429 -0
  35. package/app/src/lib/components/PageCard.svelte +3 -4
  36. package/app/src/lib/components/PreviewPane.svelte +19 -1
  37. package/app/src/lib/components/RuneAttributes.svelte +249 -100
  38. package/app/src/lib/editor/block-parser.ts +463 -0
  39. package/app/src/lib/preview/block-renderer.ts +30 -14
  40. package/dist/community-tags-builder.d.ts.map +1 -1
  41. package/dist/community-tags-builder.js +5 -1
  42. package/dist/community-tags-builder.js.map +1 -1
  43. package/dist/server.d.ts +1 -0
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +92 -6
  46. package/dist/server.js.map +1 -1
  47. package/package.json +6 -6
  48. package/preview-runtime/App.svelte +2 -0
  49. package/app/dist/assets/index-DlrXwdpb.css +0 -1
  50. package/app/dist/assets/index-GlUHQ_jL.js +0 -324
@@ -8,12 +8,21 @@
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, type SectionMapping, type ActionMapping, type CommandMapping } 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';
22
+ import InsertBlockDialog from './InsertBlockDialog.svelte';
23
+ import InlineEditPopover from './InlineEditPopover.svelte';
24
+ import ActionEditPopover from './ActionEditPopover.svelte';
25
+ import CodeEditPopover from './CodeEditPopover.svelte';
17
26
 
18
27
  interface Props {
19
28
  bodyContent: string;
@@ -28,6 +37,7 @@
28
37
  readOnly?: boolean;
29
38
  communityTags?: Record<string, unknown> | null;
30
39
  communityPostTransforms?: Record<string, Function> | null;
40
+ communityStyles?: Record<string, Record<string, unknown>> | null;
31
41
  aggregated?: Record<string, unknown>;
32
42
  }
33
43
 
@@ -44,6 +54,7 @@
44
54
  readOnly = false,
45
55
  communityTags = null,
46
56
  communityPostTransforms = null,
57
+ communityStyles = null,
47
58
  aggregated = {},
48
59
  }: Props = $props();
49
60
 
@@ -96,17 +107,14 @@
96
107
  reconcileIds(blocks, newBlocks);
97
108
  blocks = newBlocks;
98
109
  lastParsedSource = body;
99
- // Close edit panel when switching files
110
+ // Close edit panel and inline popover when switching files
100
111
  activeIndex = null;
112
+ editingFrontmatter = false;
113
+ anchorPoint = null;
114
+ inlineEdit = null;
101
115
  }
102
116
  });
103
117
 
104
- // Sync edit panel open state to global state (for layout adjustments)
105
- $effect(() => {
106
- editorState.editPanelOpen = !readOnly && (activeIndex !== null || editingFrontmatter);
107
- return () => { editorState.editPanelOpen = false; };
108
- });
109
-
110
118
  /** Sync blocks back to source text */
111
119
  function syncToSource() {
112
120
  const newSource = serializeBlocks(blocks);
@@ -126,29 +134,99 @@
126
134
  // ── Active block (rail selection) ────────────────────────────
127
135
 
128
136
  let activeIndex: number | null = $state(null);
137
+ let hoveredIndex: number | null = $state(null);
138
+ let anchorPoint: { x: number; y: number } | null = $state(null);
139
+ let pendingRuneIndex: number | null = $state(null);
140
+ let editSessionId: number = $state(0);
141
+
142
+ // ── Popover positioning ─────────────────────────────────────
143
+
144
+ const POPOVER_WIDTH = 420;
145
+ const POPOVER_GAP = 12;
129
146
 
130
- function toggleBlock(index: number) {
147
+ let popoverStyle = $derived.by(() => {
148
+ if (!anchorPoint) return '';
149
+
150
+ const vw = window.innerWidth;
151
+ const vh = window.innerHeight;
152
+
153
+ // Prefer placing to the right of the click point
154
+ let left = anchorPoint.x + POPOVER_GAP;
155
+ if (left + POPOVER_WIDTH > vw - 16) {
156
+ left = anchorPoint.x - POPOVER_WIDTH - POPOVER_GAP;
157
+ }
158
+ if (left < 16) {
159
+ left = vw - POPOVER_WIDTH - 16;
160
+ }
161
+
162
+ // Vertical: start at click Y, clamped to viewport
163
+ let top = anchorPoint.y;
164
+ const maxH = vh - 120;
165
+ const maxTop = vh - Math.min(600, maxH) - 16;
166
+ if (top > maxTop) top = maxTop;
167
+ if (top < 60) top = 60;
168
+
169
+ return `left: ${left}px; top: ${top}px; max-height: min(600px, ${maxH}px);`;
170
+ });
171
+
172
+ function toggleBlock(index: number, x: number, y: number) {
131
173
  editingFrontmatter = false;
132
- activeIndex = activeIndex === index ? null : index;
174
+ if (activeIndex === index) {
175
+ activeIndex = null;
176
+ anchorPoint = null;
177
+ pendingRuneIndex = null;
178
+ } else {
179
+ editSessionId++;
180
+ activeIndex = index;
181
+ anchorPoint = { x, y };
182
+ pendingRuneIndex = null;
183
+ }
133
184
  }
134
185
 
135
- function toggleFrontmatter() {
186
+ function handleRuneClick(index: number, x: number, y: number, nestedRuneIndex?: number) {
187
+ editingFrontmatter = false;
188
+ editSessionId++;
189
+ activeIndex = index;
190
+ anchorPoint = { x, y };
191
+ pendingRuneIndex = nestedRuneIndex ?? null;
192
+ }
193
+
194
+ function toggleFrontmatter(e: MouseEvent) {
136
195
  activeIndex = null;
137
- editingFrontmatter = !editingFrontmatter;
196
+ if (editingFrontmatter) {
197
+ editingFrontmatter = false;
198
+ anchorPoint = null;
199
+ } else {
200
+ editingFrontmatter = true;
201
+ anchorPoint = { x: e.clientX, y: e.clientY };
202
+ }
138
203
  }
139
204
 
140
205
  function handleKeydown(e: KeyboardEvent) {
141
206
  if (readOnly) return;
142
207
  if (e.key === 'Escape') {
143
- if (showInsertMenu) {
144
- closeInsertMenu();
145
- } else if (activeIndex !== null || editingFrontmatter) {
208
+ if (activeIndex !== null || editingFrontmatter) {
146
209
  activeIndex = null;
147
210
  editingFrontmatter = false;
211
+ anchorPoint = null;
212
+ pendingRuneIndex = null;
148
213
  }
149
214
  }
150
215
  }
151
216
 
217
+ function handleListScroll() {
218
+ if (activeIndex !== null || editingFrontmatter) {
219
+ activeIndex = null;
220
+ editingFrontmatter = false;
221
+ anchorPoint = null;
222
+ pendingRuneIndex = null;
223
+ }
224
+ }
225
+
226
+ function handleResize() {
227
+ if (anchorPoint) anchorPoint = { ...anchorPoint };
228
+ }
229
+
152
230
  // ── Block operations ─────────────────────────────────────────
153
231
 
154
232
  function handleUpdateBlock(index: number, updated: ParsedBlock) {
@@ -176,6 +254,9 @@
176
254
 
177
255
  function handleDragStart(e: DragEvent, index: number) {
178
256
  dragIndex = index;
257
+ activeIndex = null;
258
+ editingFrontmatter = false;
259
+ anchorPoint = null;
179
260
  if (e.dataTransfer) {
180
261
  e.dataTransfer.effectAllowed = 'move';
181
262
  e.dataTransfer.setData('text/plain', String(index));
@@ -235,13 +316,6 @@
235
316
  insertAtIndex = null;
236
317
  }
237
318
 
238
- function handleClickOutside(e: MouseEvent) {
239
- if (!showInsertMenu) return;
240
- const target = e.target as HTMLElement;
241
- if (target.closest('.insert-menu--floating') || target.closest('.block-editor__insert-dot')) return;
242
- closeInsertMenu();
243
- }
244
-
245
319
  function insertBlock(type: 'heading' | 'paragraph' | 'fence' | 'hr' | 'rune', runeName?: string) {
246
320
  let newBlock: ParsedBlock;
247
321
  const id = `blk_new_${Date.now()}`;
@@ -343,6 +417,182 @@
343
417
  syncToSource();
344
418
  }
345
419
 
420
+ // ── Inline section editing ──────────────────────────────────
421
+
422
+ let inlineEdit: {
423
+ blockIndex: number;
424
+ dataName: string;
425
+ inlineSource: string;
426
+ rect: DOMRect;
427
+ mapping: SectionMapping;
428
+ } | null = $state(null);
429
+
430
+ function handleSectionClick(index: number, info: SectionClickInfo) {
431
+ const block = blocks[index];
432
+ if (block.type !== 'rune') return;
433
+ const rb = block as RuneBlock;
434
+
435
+ if (info.editType === 'link') {
436
+ const mapping = findActionMapping(rb.innerContent, info.text, info.href ?? '');
437
+ if (!mapping) return;
438
+
439
+ actionEdit = {
440
+ blockIndex: index,
441
+ rect: info.rect,
442
+ mapping,
443
+ };
444
+ return;
445
+ }
446
+
447
+ if (info.editType === 'code') {
448
+ const mapping = findCommandMapping(rb.innerContent, info.text);
449
+ if (!mapping) return;
450
+
451
+ commandEdit = {
452
+ blockIndex: index,
453
+ rect: info.rect,
454
+ mapping,
455
+ };
456
+ return;
457
+ }
458
+
459
+ // Default: inline text editing
460
+ const mapping = findSectionMapping(rb.innerContent, info.dataName, info.text);
461
+ if (!mapping) return;
462
+
463
+ inlineEdit = {
464
+ blockIndex: index,
465
+ dataName: info.dataName,
466
+ inlineSource: mapping.inlineSource,
467
+ rect: info.rect,
468
+ mapping,
469
+ };
470
+ }
471
+
472
+ function handleInlineEditChange(newInlineSource: string) {
473
+ if (!inlineEdit) return;
474
+ const block = blocks[inlineEdit.blockIndex];
475
+ if (block.type !== 'rune') return;
476
+ const rb = block as RuneBlock;
477
+
478
+ const newInner = applySectionEdit(rb.innerContent, inlineEdit.mapping, newInlineSource);
479
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
480
+ updated.source = rebuildRuneSource(updated);
481
+
482
+ // Update the mapping to reflect the new source so subsequent edits work
483
+ inlineEdit = {
484
+ ...inlineEdit,
485
+ inlineSource: newInlineSource,
486
+ mapping: {
487
+ ...inlineEdit.mapping,
488
+ text: stripInlineMarkdown(newInlineSource),
489
+ source: inlineEdit.mapping.sourcePrefix + newInlineSource,
490
+ inlineSource: newInlineSource,
491
+ },
492
+ };
493
+
494
+ handleUpdateBlock(inlineEdit.blockIndex, updated);
495
+ }
496
+
497
+ function closeInlineEdit() {
498
+ inlineEdit = null;
499
+ }
500
+
501
+ // ── Action item editing ────────────────────────────────────
502
+
503
+ let actionEdit: {
504
+ blockIndex: number;
505
+ rect: DOMRect;
506
+ mapping: ActionMapping;
507
+ } | null = $state(null);
508
+
509
+ function handleActionEditChange(newText: string, newHref: string) {
510
+ if (!actionEdit) return;
511
+ const block = blocks[actionEdit.blockIndex];
512
+ if (block.type !== 'rune') return;
513
+ const rb = block as RuneBlock;
514
+
515
+ const newInner = applyActionEdit(rb.innerContent, actionEdit.mapping, newText, newHref);
516
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
517
+ updated.source = rebuildRuneSource(updated);
518
+
519
+ handleUpdateBlock(actionEdit.blockIndex, updated);
520
+ }
521
+
522
+ function handleActionRemove() {
523
+ if (!actionEdit) return;
524
+ const blockIndex = actionEdit.blockIndex;
525
+ const block = blocks[blockIndex];
526
+ if (block.type !== 'rune') return;
527
+ const rb = block as RuneBlock;
528
+
529
+ // Remove the entire list item line from the inner content
530
+ const newInner = rb.innerContent.replace(actionEdit.mapping.source + '\n', '').replace(actionEdit.mapping.source, '');
531
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
532
+ updated.source = rebuildRuneSource(updated);
533
+
534
+ actionEdit = null;
535
+ handleUpdateBlock(blockIndex, updated);
536
+ }
537
+
538
+ function closeActionEdit() {
539
+ actionEdit = null;
540
+ }
541
+
542
+ // ── Command (code block) editing ──────────────────────────
543
+
544
+ let commandEdit: {
545
+ blockIndex: number;
546
+ rect: DOMRect;
547
+ mapping: CommandMapping;
548
+ } | null = $state(null);
549
+
550
+ function handleCommandEditChange(newCode: string) {
551
+ if (!commandEdit) return;
552
+ const block = blocks[commandEdit.blockIndex];
553
+ if (block.type !== 'rune') return;
554
+ const rb = block as RuneBlock;
555
+
556
+ const newInner = applyCommandEdit(rb.innerContent, commandEdit.mapping, newCode);
557
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
558
+ updated.source = rebuildRuneSource(updated);
559
+
560
+ handleUpdateBlock(commandEdit.blockIndex, updated);
561
+ }
562
+
563
+ function handleCommandRemove() {
564
+ if (!commandEdit) return;
565
+ const blockIndex = commandEdit.blockIndex;
566
+ const block = blocks[blockIndex];
567
+ if (block.type !== 'rune') return;
568
+ const rb = block as RuneBlock;
569
+
570
+ // Remove the entire fenced code block from the inner content
571
+ const newInner = rb.innerContent.replace(commandEdit.mapping.source + '\n', '').replace(commandEdit.mapping.source, '');
572
+ const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
573
+ updated.source = rebuildRuneSource(updated);
574
+
575
+ commandEdit = null;
576
+ handleUpdateBlock(blockIndex, updated);
577
+ }
578
+
579
+ function closeCommandEdit() {
580
+ commandEdit = null;
581
+ }
582
+
583
+ // ── Field edit from Structure tab ──────────────────────────────
584
+
585
+ function handleFieldEdit(dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) {
586
+ if (activeIndex === null) return;
587
+ inlineEdit = {
588
+ blockIndex: activeIndex,
589
+ dataName,
590
+ inlineSource,
591
+ rect,
592
+ mapping,
593
+ };
594
+ }
595
+
346
596
  // Group runes by category for the insert menu
347
597
  let runesByCategory = $derived.by(() => {
348
598
  const map = new Map<string, RuneInfo[]>();
@@ -355,7 +605,7 @@
355
605
  });
356
606
  </script>
357
607
 
358
- <svelte:window onkeydown={handleKeydown} onmousedown={handleClickOutside} />
608
+ <svelte:window onkeydown={handleKeydown} onresize={handleResize} />
359
609
 
360
610
  <div class="block-editor">
361
611
  {#if blocks.length === 0 && !readOnly}
@@ -367,14 +617,14 @@
367
617
  <line x1="14" y1="30" x2="22" y2="30" />
368
618
  </svg>
369
619
  <span class="block-editor__empty-text">No content blocks yet</span>
370
- <span class="block-editor__empty-hint">Click a + dot in the rail to add blocks</span>
620
+ <span class="block-editor__empty-hint">Use the + button to add your first block</span>
371
621
  </div>
372
622
  {/if}
373
623
 
374
- <div class="block-editor__stage" class:editing={!readOnly && (activeIndex !== null || editingFrontmatter)}>
375
- <!-- Scrollable list + rail area -->
376
- <div class="block-editor__scroll" class:has-rail={!readOnly}>
377
- <div class="block-editor__list-wrap">
624
+ <div class="block-editor__stage">
625
+ <!-- Scrollable block list -->
626
+ <div class="block-editor__scroll">
627
+ <div class="block-editor__list-wrap" onscroll={handleListScroll}>
378
628
  <!-- Frontmatter summary header (blocks mode only) -->
379
629
  {#if !readOnly}
380
630
  <div class="block-editor__fm-header">
@@ -387,7 +637,7 @@
387
637
  <button
388
638
  class="block-editor__fm-edit"
389
639
  class:active={editingFrontmatter}
390
- onclick={toggleFrontmatter}
640
+ onclick={(e) => toggleFrontmatter(e)}
391
641
  title="Edit frontmatter"
392
642
  >
393
643
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
@@ -398,90 +648,15 @@
398
648
  </div>
399
649
  {/if}
400
650
 
401
- {#snippet insertMenuContent()}
402
- <div class="insert-menu__section">
403
- <span class="insert-menu__label">Content</span>
404
- <div class="insert-menu__grid">
405
- <button class="insert-menu__btn" onclick={() => insertBlock('heading')}>
406
- <svg class="insert-menu__icon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
407
- <path d="M3 3v10M13 3v10M3 8h10" />
408
- </svg>
409
- Heading
410
- </button>
411
- <button class="insert-menu__btn" onclick={() => insertBlock('paragraph')}>
412
- <svg class="insert-menu__icon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
413
- <line x1="2" y1="4" x2="14" y2="4" />
414
- <line x1="2" y1="8" x2="14" y2="8" />
415
- <line x1="2" y1="12" x2="10" y2="12" />
416
- </svg>
417
- Paragraph
418
- </button>
419
- <button class="insert-menu__btn" onclick={() => insertBlock('fence')}>
420
- <svg class="insert-menu__icon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
421
- <polyline points="5 4 2 8 5 12" />
422
- <polyline points="11 4 14 8 11 12" />
423
- </svg>
424
- Code Block
425
- </button>
426
- <button class="insert-menu__btn" onclick={() => insertBlock('hr')}>
427
- <svg class="insert-menu__icon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
428
- <line x1="2" y1="8" x2="14" y2="8" />
429
- </svg>
430
- Divider
431
- </button>
432
- </div>
433
- </div>
434
- {#each [...runesByCategory.entries()] as [category, categoryRunes]}
435
- <div class="insert-menu__section">
436
- <span class="insert-menu__label">{category}</span>
437
- <div class="insert-menu__grid">
438
- {#each categoryRunes as rune}
439
- <button
440
- class="insert-menu__btn insert-menu__btn--rune"
441
- onclick={() => insertBlock('rune', rune.name)}
442
- >
443
- <span class="insert-menu__rune-dot"></span>
444
- <span class="insert-menu__rune-info">
445
- <span class="insert-menu__rune-name">{rune.name}</span>
446
- {#if rune.description}
447
- <span class="insert-menu__rune-desc">{rune.description}</span>
448
- {/if}
449
- </span>
450
- </button>
451
- {/each}
452
- </div>
453
- </div>
454
- {/each}
455
- <button class="insert-menu__close" onclick={closeInsertMenu}>&times; Close</button>
456
- {/snippet}
457
-
458
- {#snippet insertZone(pos: number)}
459
- <div class="block-editor__insert-zone">
460
- <div class="block-editor__insert-zone-spacer"></div>
461
- <button
462
- class="block-editor__insert-dot"
463
- onclick={() => openInsertAt(pos)}
464
- title="Insert block"
465
- >
466
- <span class="block-editor__dot-icon"></span>
467
- </button>
468
- {#if showInsertMenu && insertAtIndex === pos}
469
- <div class="insert-menu insert-menu--floating">
470
- {@render insertMenuContent()}
471
- </div>
472
- {/if}
473
- </div>
474
- {/snippet}
475
-
476
- {#if !readOnly && showInsertMenuProp}
477
- {@render insertZone(0)}
478
- {/if}
479
-
480
651
  {#each blocks as block, i (block.id)}
481
652
  <div
482
653
  class="block-editor__row"
654
+ class:hovered={!readOnly && hoveredIndex === i}
655
+ class:active={!readOnly && activeIndex === i}
483
656
  class:drag-source={!readOnly && dragIndex === i}
484
657
  class:drag-over={!readOnly && dropIndex === i && dragIndex !== i}
658
+ onmouseenter={() => { if (!readOnly) hoveredIndex = i; }}
659
+ onmouseleave={() => { if (!readOnly && hoveredIndex === i) hoveredIndex = null; }}
485
660
  >
486
661
  <div class="block-editor__block-cell">
487
662
  <BlockCard
@@ -492,51 +667,126 @@
492
667
  {highlightTransform}
493
668
  {communityTags}
494
669
  {communityPostTransforms}
670
+ {communityStyles}
495
671
  {aggregated}
672
+ {readOnly}
673
+ onsectionclick={readOnly ? undefined : (info) => handleSectionClick(i, info)}
674
+ onruneclick={readOnly ? undefined : (info) => handleRuneClick(i, info.x, info.y, info.nestedRuneIndex)}
496
675
  ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
497
676
  ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
498
677
  ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
499
678
  />
500
679
  </div>
680
+ {#if !readOnly && showInsertMenuProp}
681
+ <!-- Insert markers — top and bottom edges -->
682
+ <button
683
+ class="block-editor__insert-marker block-editor__insert-marker--top"
684
+ onclick={() => openInsertAt(i)}
685
+ title="Insert block above"
686
+ >
687
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
688
+ <line x1="5" y1="2" x2="5" y2="8" />
689
+ <line x1="2" y1="5" x2="8" y2="5" />
690
+ </svg>
691
+ </button>
692
+ <button
693
+ class="block-editor__insert-marker block-editor__insert-marker--bottom"
694
+ onclick={() => openInsertAt(i + 1)}
695
+ title="Insert block below"
696
+ >
697
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
698
+ <line x1="5" y1="2" x2="5" y2="8" />
699
+ <line x1="2" y1="5" x2="8" y2="5" />
700
+ </svg>
701
+ </button>
702
+ {/if}
501
703
  {#if !readOnly}
704
+ <!-- Block label — slides in from right on hover, pinned when active -->
502
705
  <button
503
- class="block-editor__rail-label"
504
- class:active={activeIndex === i}
505
- onclick={() => toggleBlock(i)}
706
+ class="block-editor__hover-label"
707
+ onclick={(e) => toggleBlock(i, e.clientX, e.clientY)}
506
708
  aria-pressed={activeIndex === i}
507
709
  >
508
710
  {blockLabel(block)}
509
711
  </button>
510
712
  {/if}
511
713
  </div>
512
- {#if !readOnly && showInsertMenuProp}
513
- {@render insertZone(i + 1)}
514
- {/if}
515
714
  {/each}
516
715
  </div>
517
716
  </div>
518
717
 
519
- <!-- Edit panel — slides in from the right (blocks mode only) -->
520
- {#if !readOnly}
521
- <div class="block-editor__edit-panel">
522
- {#if editingFrontmatter}
523
- <FrontmatterEditPanel
524
- onclose={() => { editingFrontmatter = false; }}
525
- />
526
- {:else if activeIndex !== null && blocks[activeIndex]}
718
+ </div>
719
+
720
+ <!-- Popover edit panel — anchored to click position -->
721
+ {#if !readOnly && (activeIndex !== null || editingFrontmatter)}
722
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
723
+ <div
724
+ class="block-editor__popover-backdrop"
725
+ onmousedown={() => { activeIndex = null; editingFrontmatter = false; anchorPoint = null; pendingRuneIndex = null; }}
726
+ ></div>
727
+ <div class="block-editor__popover" style={popoverStyle}>
728
+ {#if editingFrontmatter}
729
+ <FrontmatterEditPanel
730
+ onclose={() => { editingFrontmatter = false; anchorPoint = null; }}
731
+ />
732
+ {:else if activeIndex !== null && blocks[activeIndex]}
733
+ {#key editSessionId}
527
734
  <BlockEditPanel
528
735
  block={blocks[activeIndex]}
529
736
  {runeMap}
530
737
  runes={() => runes}
531
- {aggregated}
738
+ {aggregated}
739
+ initialRuneIndex={pendingRuneIndex}
532
740
  onupdate={(updated) => handleUpdateBlock(activeIndex!, updated)}
533
- onremove={() => { const idx = activeIndex!; activeIndex = null; handleRemoveBlock(idx); }}
534
- onclose={() => { activeIndex = null; }}
741
+ onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null; handleRemoveBlock(idx); }}
742
+ onclose={() => { activeIndex = null; anchorPoint = null; pendingRuneIndex = null; }}
743
+ oneditfield={handleFieldEdit}
535
744
  />
536
- {/if}
537
- </div>
538
- {/if}
539
- </div>
745
+ {/key}
746
+ {/if}
747
+ </div>
748
+ {/if}
749
+
750
+ {#if showInsertMenu}
751
+ <InsertBlockDialog
752
+ {runes}
753
+ {runesByCategory}
754
+ oninsert={insertBlock}
755
+ onclose={closeInsertMenu}
756
+ />
757
+ {/if}
758
+
759
+ {#if inlineEdit}
760
+ <InlineEditPopover
761
+ anchorRect={inlineEdit.rect}
762
+ dataName={inlineEdit.dataName}
763
+ inlineSource={inlineEdit.inlineSource}
764
+ onchange={handleInlineEditChange}
765
+ onclose={closeInlineEdit}
766
+ />
767
+ {/if}
768
+
769
+ {#if actionEdit}
770
+ <ActionEditPopover
771
+ anchorRect={actionEdit.rect}
772
+ text={actionEdit.mapping.text}
773
+ href={actionEdit.mapping.href}
774
+ onchange={handleActionEditChange}
775
+ onremove={handleActionRemove}
776
+ onclose={closeActionEdit}
777
+ />
778
+ {/if}
779
+
780
+ {#if commandEdit}
781
+ <CodeEditPopover
782
+ anchorRect={commandEdit.rect}
783
+ code={commandEdit.mapping.code}
784
+ language={commandEdit.mapping.language}
785
+ onchange={handleCommandEditChange}
786
+ onremove={handleCommandRemove}
787
+ onclose={closeCommandEdit}
788
+ />
789
+ {/if}
540
790
  </div>
541
791
 
542
792
 
@@ -557,58 +807,28 @@
557
807
  position: relative;
558
808
  }
559
809
 
560
- /* Scrollable block list + rail area */
810
+ /* Scrollable block list */
561
811
  .block-editor__scroll {
562
812
  flex: 1;
563
813
  min-height: 0;
564
814
  display: flex;
565
815
  flex-direction: column;
566
- transition: margin-right var(--ed-transition-slow);
567
816
  }
568
817
 
569
818
  .block-editor__list-wrap {
570
819
  width: 100%;
571
- padding: var(--ed-space-4);
572
- padding-right: 0;
820
+ padding: 0;
573
821
  flex: 1;
574
822
  min-height: 0;
575
823
  overflow-y: auto;
576
824
  }
577
825
 
578
- /* Rail column: vertical border + diagonal stripe background */
579
- .block-editor__scroll.has-rail .block-editor__list-wrap {
580
- background-origin: content-box;
581
- background-clip: border-box;
582
- background:
583
- /* Vertical border at left edge of rail */
584
- linear-gradient(to right,
585
- transparent calc(100% - 91px),
586
- var(--ed-border-default) calc(100% - 91px),
587
- var(--ed-border-default) calc(100% - 90px),
588
- transparent calc(100% - 90px)
589
- ),
590
- /* Solid background masking stripes in the content area */
591
- linear-gradient(to right,
592
- var(--ed-surface-0) calc(100% - 90px),
593
- transparent calc(100% - 90px)
594
- ),
595
- /* Stripe pattern */
596
- repeating-linear-gradient(
597
- -45deg,
598
- transparent,
599
- transparent 4px,
600
- rgba(0, 0, 0, 0.04) 4px,
601
- rgba(0, 0, 0, 0.04) 5px
602
- );
603
- }
604
-
605
826
  /* Frontmatter summary header */
606
827
  .block-editor__fm-header {
607
828
  display: flex;
608
829
  align-items: center;
609
830
  gap: var(--ed-space-3);
610
- padding: var(--ed-space-3) var(--ed-space-4);
611
- border-bottom: 1px solid var(--ed-border-subtle);
831
+ padding: var(--ed-space-5) var(--ed-space-5);
612
832
  margin-bottom: var(--ed-space-2);
613
833
  }
614
834
 
@@ -665,15 +885,13 @@
665
885
  color: var(--ed-accent);
666
886
  }
667
887
 
668
- /* Block row: preview cell + rail label */
888
+ /* Block row */
669
889
  .block-editor__row {
670
- display: flex;
671
- align-items: flex-start;
890
+ position: relative;
672
891
  transition: transform var(--ed-transition-fast), opacity var(--ed-transition-fast);
673
892
  }
674
893
 
675
894
  .block-editor__block-cell {
676
- flex: 1;
677
895
  min-width: 0;
678
896
  }
679
897
 
@@ -688,145 +906,124 @@
688
906
  padding-top: 2px;
689
907
  }
690
908
 
691
- /* Rail labels aligned to the right of each block */
692
- .block-editor__rail-label {
693
- width: 80px;
694
- flex-shrink: 0;
695
- margin-left: var(--ed-space-5);
696
- padding: 0.5rem 0.6rem 0.5rem 1rem;
909
+ /* ── Hover controls (insert markers + label) ─────────────── */
910
+
911
+ /* Shared base for insert markers and label */
912
+ .block-editor__insert-marker,
913
+ .block-editor__hover-label {
914
+ position: absolute;
915
+ right: 20px;
916
+ z-index: 2;
917
+ border: none;
918
+ cursor: pointer;
919
+ border-radius: 9999px;
920
+ background: var(--ed-surface-2);
921
+ color: var(--ed-text-muted);
697
922
  font-size: 10px;
698
923
  font-weight: 600;
699
- color: var(--ed-text-muted);
700
924
  text-transform: uppercase;
701
925
  letter-spacing: 0.03em;
702
- text-align: left;
703
- background: transparent;
704
- border: none;
705
- cursor: pointer;
706
- white-space: nowrap;
707
- overflow: hidden;
708
- text-overflow: ellipsis;
709
- transition: color var(--ed-transition-fast);
710
- align-self: stretch;
711
- line-height: 1.3;
712
- }
713
-
714
- .block-editor__rail-label:hover {
715
- color: var(--ed-text-secondary);
716
- }
717
-
718
- .block-editor__rail-label.active {
719
- color: var(--ed-accent);
720
- font-weight: 700;
926
+ opacity: 0;
927
+ pointer-events: none;
928
+ transition: opacity 100ms ease-out, transform 100ms ease-out, background 100ms ease-out, color 100ms ease-out;
721
929
  }
722
930
 
723
- /* Insert zonesbetween blocks in the rail */
724
- .block-editor__insert-zone {
931
+ /* Insert markerstop/bottom edges */
932
+ .block-editor__insert-marker {
725
933
  display: flex;
726
934
  align-items: center;
727
- height: 12px;
728
- position: relative;
729
- overflow: visible;
730
- z-index: 2;
935
+ justify-content: center;
936
+ width: 22px;
937
+ height: 22px;
938
+ padding: 0;
939
+ color: var(--ed-text-tertiary);
731
940
  }
732
941
 
733
- .block-editor__insert-zone-spacer {
734
- flex: 1;
942
+ .block-editor__insert-marker--top {
943
+ top: -11px;
735
944
  }
736
945
 
737
- .block-editor__insert-dot {
738
- width: 80px;
739
- flex-shrink: 0;
740
- margin-left: var(--ed-space-5);
741
- display: flex;
742
- align-items: center;
743
- justify-content: flex-start;
744
- border: none;
745
- background: transparent;
746
- cursor: pointer;
747
- height: 100%;
748
- padding: 0;
749
- position: relative;
750
- overflow: visible;
946
+ .block-editor__insert-marker--bottom {
947
+ bottom: -11px;
751
948
  }
752
949
 
753
- .block-editor__dot-icon {
754
- position: absolute;
755
- left: 1px;
950
+ /* Label — right side, vertically centered */
951
+ .block-editor__hover-label {
756
952
  top: 50%;
757
- transform: translate(-50%, -50%);
758
- width: 5px;
759
- height: 5px;
760
- border-radius: 50%;
761
- background: var(--ed-text-muted);
762
- transition: width var(--ed-transition-fast), height var(--ed-transition-fast), background var(--ed-transition-fast);
953
+ transform: translateX(6px) translateY(-50%);
954
+ padding: 4px 10px;
955
+ white-space: nowrap;
956
+ line-height: 1.3;
763
957
  }
764
958
 
765
- .block-editor__insert-dot:hover .block-editor__dot-icon {
766
- width: 18px;
767
- height: 18px;
768
- background: var(--ed-accent);
959
+ /* Show controls on hover */
960
+ .block-editor__row.hovered .block-editor__insert-marker,
961
+ .block-editor__row.hovered .block-editor__hover-label {
962
+ opacity: 1;
963
+ pointer-events: auto;
769
964
  }
770
965
 
771
- .block-editor__dot-icon::before,
772
- .block-editor__dot-icon::after {
773
- content: '';
774
- position: absolute;
775
- background: white;
776
- border-radius: 1px;
777
- opacity: 0;
778
- transition: opacity var(--ed-transition-fast);
966
+ .block-editor__row.hovered .block-editor__hover-label {
967
+ transform: translateX(0) translateY(-50%);
779
968
  }
780
969
 
781
- .block-editor__dot-icon::before {
782
- width: 10px;
783
- height: 1.5px;
784
- top: 50%;
785
- left: 50%;
786
- transform: translate(-50%, -50%);
970
+ /* Pinned label when edit panel is open */
971
+ .block-editor__row.active .block-editor__hover-label {
972
+ opacity: 1;
973
+ pointer-events: auto;
974
+ transform: translateX(0) translateY(-50%);
975
+ background: var(--ed-accent-muted);
976
+ color: var(--ed-accent);
977
+ font-weight: 700;
787
978
  }
788
979
 
789
- .block-editor__dot-icon::after {
790
- width: 1.5px;
791
- height: 10px;
792
- top: 50%;
793
- left: 50%;
794
- transform: translate(-50%, -50%);
980
+ /* Insert marker hover */
981
+ .block-editor__insert-marker:hover {
982
+ background: var(--ed-accent);
983
+ color: white;
795
984
  }
796
985
 
797
- .block-editor__insert-dot:hover .block-editor__dot-icon::before,
798
- .block-editor__insert-dot:hover .block-editor__dot-icon::after {
799
- opacity: 1;
986
+ /* Label hover */
987
+ .block-editor__hover-label:hover {
988
+ color: var(--ed-text-secondary);
989
+ background: var(--ed-surface-3, var(--ed-border-default));
800
990
  }
801
991
 
802
- /* Floating insert menu — anchored to the insert zone */
803
- .insert-menu--floating {
804
- position: absolute;
805
- right: 0;
806
- top: 100%;
807
- z-index: 20;
808
- width: 360px;
809
- max-height: 400px;
810
- overflow-y: auto;
992
+ .block-editor__row.active .block-editor__hover-label:hover {
993
+ background: var(--ed-accent);
994
+ color: white;
995
+ }
996
+
997
+ /* Popover backdrop — transparent click target */
998
+ .block-editor__popover-backdrop {
999
+ position: fixed;
1000
+ inset: 0;
1001
+ z-index: 9;
811
1002
  }
812
1003
 
813
- /* Edit panelfixed to right edge of viewport, outside the card */
814
- .block-editor__edit-panel {
1004
+ /* Popover containeranchored to block card */
1005
+ .block-editor__popover {
815
1006
  position: fixed;
816
- top: 60px;
817
- right: 0;
818
- bottom: 0;
819
- width: 480px;
1007
+ width: 420px;
820
1008
  overflow-y: auto;
821
- background: var(--ed-surface-1);
822
- border-left: 1px solid var(--ed-border-default);
823
- transform: translateX(100%);
824
- transition: transform var(--ed-transition-slow);
1009
+ background: var(--ed-surface-0);
1010
+ border-radius: var(--ed-radius-lg);
1011
+ border: 1px solid var(--ed-border-default);
1012
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
1013
+ 0 8px 10px -6px rgba(0, 0, 0, 0.1);
825
1014
  z-index: 10;
1015
+ animation: popover-enter 0.15s ease-out;
826
1016
  }
827
1017
 
828
- .block-editor__stage.editing .block-editor__edit-panel {
829
- transform: translateX(0);
1018
+ @keyframes popover-enter {
1019
+ from {
1020
+ opacity: 0;
1021
+ transform: translateY(4px) scale(0.98);
1022
+ }
1023
+ to {
1024
+ opacity: 1;
1025
+ transform: translateY(0) scale(1);
1026
+ }
830
1027
  }
831
1028
 
832
1029
  /* Empty state */
@@ -855,123 +1052,4 @@
855
1052
  font-size: var(--ed-text-sm);
856
1053
  }
857
1054
 
858
- /* Insert menu (shared between floating and any future inline) */
859
- .insert-menu {
860
- border: 1px solid var(--ed-border-default);
861
- border-radius: 10px;
862
- background: var(--ed-surface-0);
863
- padding: var(--ed-space-4);
864
- display: flex;
865
- flex-direction: column;
866
- gap: var(--ed-space-3);
867
- box-shadow: var(--ed-shadow-lg);
868
- animation: menu-enter var(--ed-transition-normal);
869
- }
870
-
871
- @keyframes menu-enter {
872
- from { opacity: 0; transform: translateY(4px); }
873
- to { opacity: 1; transform: translateY(0); }
874
- }
875
-
876
- .insert-menu__section {
877
- display: flex;
878
- flex-direction: column;
879
- gap: 0.4rem;
880
- }
881
-
882
- .insert-menu__label {
883
- font-size: var(--ed-text-xs);
884
- font-weight: 600;
885
- color: var(--ed-text-muted);
886
- text-transform: uppercase;
887
- letter-spacing: 0.05em;
888
- }
889
-
890
- .insert-menu__grid {
891
- display: grid;
892
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
893
- gap: 0.35rem;
894
- }
895
-
896
- .insert-menu__btn {
897
- display: flex;
898
- align-items: center;
899
- gap: 0.4rem;
900
- padding: var(--ed-space-2) var(--ed-space-2);
901
- border: 1px solid var(--ed-border-default);
902
- border-radius: var(--ed-radius-sm);
903
- background: var(--ed-surface-1);
904
- color: var(--ed-text-secondary);
905
- font-size: var(--ed-text-sm);
906
- cursor: pointer;
907
- transition: background var(--ed-transition-fast), border-color var(--ed-transition-fast);
908
- text-align: left;
909
- }
910
-
911
- .insert-menu__btn:hover {
912
- background: var(--ed-accent-muted);
913
- border-color: var(--ed-accent);
914
- color: var(--ed-heading);
915
- }
916
-
917
- .insert-menu__icon {
918
- flex-shrink: 0;
919
- opacity: 0.6;
920
- }
921
-
922
- /* Rune buttons */
923
- .insert-menu__btn--rune {
924
- align-items: flex-start;
925
- padding: var(--ed-space-2);
926
- }
927
-
928
- .insert-menu__rune-dot {
929
- width: 6px;
930
- height: 6px;
931
- border-radius: 50%;
932
- background: var(--ed-warning);
933
- flex-shrink: 0;
934
- margin-top: 0.3rem;
935
- }
936
-
937
- .insert-menu__rune-info {
938
- display: flex;
939
- flex-direction: column;
940
- gap: 0.15rem;
941
- min-width: 0;
942
- }
943
-
944
- .insert-menu__rune-name {
945
- font-weight: 500;
946
- }
947
-
948
- .insert-menu__rune-desc {
949
- font-size: 10px;
950
- color: var(--ed-text-muted);
951
- overflow: hidden;
952
- text-overflow: ellipsis;
953
- white-space: nowrap;
954
- }
955
-
956
- .insert-menu__btn--rune:hover {
957
- background: var(--ed-warning-subtle);
958
- border-color: var(--ed-warning);
959
- }
960
-
961
- .insert-menu__close {
962
- align-self: flex-end;
963
- padding: var(--ed-space-1) var(--ed-space-2);
964
- border: none;
965
- border-radius: var(--ed-radius-sm);
966
- background: transparent;
967
- color: var(--ed-text-muted);
968
- font-size: var(--ed-text-sm);
969
- cursor: pointer;
970
- transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
971
- }
972
-
973
- .insert-menu__close:hover {
974
- background: var(--ed-surface-2);
975
- color: var(--ed-text-secondary);
976
- }
977
1055
  </style>