@refrakt-md/editor 0.8.1 → 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 (39) hide show
  1. package/app/dist/assets/{index-BgCNqcSo.js → index-80NtMar1.js} +1 -1
  2. package/app/dist/assets/index-B6H6LF1M.css +1 -0
  3. package/app/dist/assets/{index-BLuaHLN3.js → index-BDj1XPol.js} +1 -1
  4. package/app/dist/assets/{index-D_Y6J00B.js → index-BXe1fKaT.js} +1 -1
  5. package/app/dist/assets/{index-ZLvRNfLb.js → index-BfxTGrHB.js} +1 -1
  6. package/app/dist/assets/{index-D3TQo8gu.js → index-Bn8ajfVl.js} +1 -1
  7. package/app/dist/assets/{index-DgIg-QAA.js → index-CCkzIGTi.js} +2 -2
  8. package/app/dist/assets/{index-COIPZ34u.js → index-CXeK-dZx.js} +1 -1
  9. package/app/dist/assets/{index-DW2zI-Ss.js → index-CaRBCHaX.js} +1 -1
  10. package/app/dist/assets/index-Cd12jZId.js +479 -0
  11. package/app/dist/assets/{index-DmY6uqAw.js → index-Cgbvx23V.js} +1 -1
  12. package/app/dist/assets/{index-CW02bulk.js → index-D5ucdUTo.js} +1 -1
  13. package/app/dist/assets/{index-BwFn9q4x.js → index-DGYxLhpR.js} +1 -1
  14. package/app/dist/assets/{index-CqHjo2YT.js → index-DNJBunzP.js} +1 -1
  15. package/app/dist/assets/{index-CeU_s7BB.js → index-DNtuldOx.js} +1 -1
  16. package/app/dist/assets/{index-C72UC2ga.js → index-DQUOY-pF.js} +1 -1
  17. package/app/dist/assets/{index-CXFMPmtf.js → index-DskvyNKT.js} +1 -1
  18. package/app/dist/assets/{index-BBinZAiy.js → index-aPeHMqUX.js} +1 -1
  19. package/app/dist/assets/{index-DVM3uoxc.js → index-dGztG-54.js} +1 -1
  20. package/app/dist/assets/{index-DzHt8ZRh.js → index-xo7v6nRB.js} +1 -1
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/api/client.ts +32 -0
  23. package/app/src/lib/components/ActionEditPopover.svelte +41 -19
  24. package/app/src/lib/components/BlockCard.svelte +74 -17
  25. package/app/src/lib/components/BlockEditPanel.svelte +142 -9
  26. package/app/src/lib/components/BlockEditor.svelte +154 -2
  27. package/app/src/lib/components/CodeEditPopover.svelte +281 -63
  28. package/app/src/lib/components/ContentModelTree.svelte +340 -67
  29. package/app/src/lib/components/IconPickerPopover.svelte +389 -0
  30. package/app/src/lib/components/ImageEditPopover.svelte +519 -0
  31. package/app/src/lib/components/InlineEditPopover.svelte +79 -56
  32. package/app/src/lib/components/RuneAttributes.svelte +51 -0
  33. package/app/src/lib/editor/block-parser.ts +152 -7
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +129 -1
  36. package/dist/server.js.map +1 -1
  37. package/package.json +6 -6
  38. package/app/dist/assets/index-BD2EBUrQ.css +0 -1
  39. package/app/dist/assets/index-BlAOhWAQ.js +0 -453
@@ -19,9 +19,14 @@
19
19
  removeFieldContent,
20
20
  appendListItem,
21
21
  removeListItem,
22
+ reorderListItem,
23
+ appendGreedyItem,
24
+ removeGreedyItem,
25
+ reorderGreedyItem,
26
+ splitListItems,
22
27
  } from '../editor/block-parser.js';
23
- import { resolveContentStructure } from '../editor/content-model-resolver.js';
24
- import type { SectionMapping } from '../editor/section-mapper.js';
28
+ import { resolveContentStructure, type ResolvedField } from '../editor/content-model-resolver.js';
29
+ import type { SectionMapping, CommandMapping } from '../editor/section-mapper.js';
25
30
  import { stripInlineMarkdown } from '../editor/inline-markdown.js';
26
31
  import RuneAttributes from './RuneAttributes.svelte';
27
32
  import ContentTree from './ContentTree.svelte';
@@ -39,9 +44,10 @@
39
44
  onremove: () => void;
40
45
  onclose: () => void;
41
46
  oneditfield?: (dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) => void;
47
+ oneditcode?: (code: string, language: string, rect: DOMRect, mapping: CommandMapping) => void;
42
48
  }
43
49
 
44
- let { block, runeMap, runes, aggregated = {}, initialRuneIndex = null, onupdate, onremove, onclose, oneditfield }: Props = $props();
50
+ let { block, runeMap, runes, aggregated = {}, initialRuneIndex = null, onupdate, onremove, onclose, oneditfield, oneditcode }: Props = $props();
45
51
 
46
52
  let label = $derived(blockLabel(block));
47
53
 
@@ -220,18 +226,54 @@
220
226
  applyFieldChange(content => removeFieldContent(content, resolvedStructure!, fieldName, zoneName));
221
227
  }
222
228
 
229
+ function findResolvedField(fieldName: string, zoneName?: string): ResolvedField | null {
230
+ if (!resolvedStructure) return null;
231
+ if (resolvedStructure.type === 'sequence') {
232
+ return resolvedStructure.fields.find(f => f.name === fieldName) ?? null;
233
+ }
234
+ if (resolvedStructure.type === 'delimited' && zoneName) {
235
+ const zone = resolvedStructure.zones.find(z => z.name === zoneName);
236
+ return zone?.fields.find(f => f.name === fieldName) ?? null;
237
+ }
238
+ return null;
239
+ }
240
+
241
+ function isGreedyItemField(field: ResolvedField): boolean {
242
+ return field.greedy && field.match !== 'any';
243
+ }
244
+
223
245
  function handleAppendItem(fieldName: string, zoneName?: string) {
224
246
  if (!resolvedStructure) return;
225
- applyFieldChange(content => appendListItem(content, resolvedStructure!, fieldName, zoneName));
247
+ const field = findResolvedField(fieldName, zoneName);
248
+ if (field && isGreedyItemField(field)) {
249
+ applyFieldChange(content => appendGreedyItem(content, resolvedStructure!, fieldName, zoneName));
250
+ } else {
251
+ applyFieldChange(content => appendListItem(content, resolvedStructure!, fieldName, zoneName));
252
+ }
226
253
  }
227
254
 
228
255
  function handleRemoveListItem(fieldName: string, itemIndex: number, zoneName?: string) {
229
256
  if (!resolvedStructure) return;
230
- applyFieldChange(content => removeListItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
257
+ const field = findResolvedField(fieldName, zoneName);
258
+ if (field && isGreedyItemField(field)) {
259
+ applyFieldChange(content => removeGreedyItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
260
+ } else {
261
+ applyFieldChange(content => removeListItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
262
+ }
231
263
  }
232
264
 
233
- function handleEditField(fieldName: string, rect: DOMRect, zoneName?: string) {
234
- if (!resolvedStructure || !oneditfield) return;
265
+ function handleReorderListItem(fieldName: string, fromIndex: number, toIndex: number, zoneName?: string) {
266
+ if (!resolvedStructure) return;
267
+ const field = findResolvedField(fieldName, zoneName);
268
+ if (field && isGreedyItemField(field)) {
269
+ applyFieldChange(content => reorderGreedyItem(content, resolvedStructure!, fieldName, fromIndex, toIndex, zoneName));
270
+ } else {
271
+ applyFieldChange(content => reorderListItem(content, resolvedStructure!, fieldName, fromIndex, toIndex, zoneName));
272
+ }
273
+ }
274
+
275
+ function handleEditField(fieldName: string, rect: DOMRect, zoneName?: string, nodeIndex?: number) {
276
+ if (!resolvedStructure) return;
235
277
  // Find the field in the resolved structure
236
278
  let field;
237
279
  if (resolvedStructure.type === 'sequence') {
@@ -240,9 +282,30 @@
240
282
  const zone = resolvedStructure.zones.find(z => z.name === zoneName);
241
283
  field = zone?.fields.find(f => f.name === fieldName);
242
284
  }
243
- if (!field || !field.filled || field.nodes.length !== 1) return;
285
+ if (!field || !field.filled) return;
286
+
287
+ // Pick the target node — use nodeIndex for greedy fields, default to single-node fields
288
+ const targetIndex = nodeIndex ?? 0;
289
+ if (targetIndex >= field.nodes.length) return;
290
+ if (nodeIndex === undefined && field.nodes.length !== 1) return;
291
+
292
+ const targetNode = field.nodes[targetIndex];
293
+ const source = targetNode.source;
294
+
295
+ // Fence nodes → open code editor instead of inline text editor
296
+ if (targetNode.type === 'fence' && oneditcode) {
297
+ const code = targetNode.fenceCode ?? '';
298
+ const language = targetNode.fenceLanguage ?? '';
299
+ const opener = source.split('\n')[0];
300
+ const delimMatch = opener.match(/^(`{3,}|~{3,})/);
301
+ const delimiter = delimMatch ? delimMatch[1] : '```';
302
+ const mapping: CommandMapping = { source, code, language, opener, delimiter };
303
+ oneditcode(code, language, rect, mapping);
304
+ return;
305
+ }
306
+
307
+ if (!oneditfield) return;
244
308
 
245
- const source = field.nodes[0].source;
246
309
  const trimmed = source.trim();
247
310
 
248
311
  // Strip markdown prefix (heading markers, blockquote markers)
@@ -270,6 +333,70 @@
270
333
  oneditfield(fieldName, inlineContent, rect, mapping);
271
334
  }
272
335
 
336
+ function handleEditListItem(fieldName: string, itemIndex: number, rect: DOMRect, zoneName?: string) {
337
+ if (!resolvedStructure || !oneditfield) return;
338
+ // Find the field in the resolved structure
339
+ let field;
340
+ if (resolvedStructure.type === 'sequence') {
341
+ field = resolvedStructure.fields.find(f => f.name === fieldName);
342
+ } else if (resolvedStructure.type === 'delimited' && zoneName) {
343
+ const zone = resolvedStructure.zones.find(z => z.name === zoneName);
344
+ field = zone?.fields.find(f => f.name === fieldName);
345
+ }
346
+ if (!field || !field.filled || field.nodes.length === 0) return;
347
+
348
+ const listNode = field.nodes[0];
349
+ const items = splitListItems(listNode.source);
350
+ if (itemIndex < 0 || itemIndex >= items.length) return;
351
+
352
+ const itemSource = items[itemIndex];
353
+ // Strip the list marker to get inline content
354
+ const markerMatch = itemSource.match(/^([-*+]\s+|\d+\.\s+)/);
355
+ const prefix = markerMatch ? markerMatch[1] : '';
356
+ const inlineContent = markerMatch ? itemSource.slice(prefix.length) : itemSource;
357
+
358
+ const mapping: SectionMapping = {
359
+ dataName: `${fieldName}[${itemIndex}]`,
360
+ text: stripInlineMarkdown(inlineContent),
361
+ source: itemSource,
362
+ sourcePrefix: prefix,
363
+ inlineSource: inlineContent,
364
+ };
365
+ oneditfield(`${fieldName}[${itemIndex}]`, inlineContent, rect, mapping);
366
+ }
367
+
368
+ function handleNavigateRune(fieldName: string, nodeIndex: number, zoneName?: string) {
369
+ if (!resolvedStructure) return;
370
+
371
+ // Find the field
372
+ let field;
373
+ if (resolvedStructure.type === 'sequence') {
374
+ field = resolvedStructure.fields.find(f => f.name === fieldName);
375
+ } else if (resolvedStructure.type === 'delimited' && zoneName) {
376
+ const zone = resolvedStructure.zones.find(z => z.name === zoneName);
377
+ field = zone?.fields.find(f => f.name === fieldName);
378
+ }
379
+ if (!field || nodeIndex < 0 || nodeIndex >= field.nodes.length) return;
380
+
381
+ const targetNode = field.nodes[nodeIndex];
382
+ if (targetNode.type !== 'rune') return;
383
+
384
+ // Find this node's index in the effectiveContentTree
385
+ const tree = effectiveContentTree;
386
+ const treeIndex = tree.findIndex(n => n.source === targetNode.source);
387
+ if (treeIndex === -1) return;
388
+
389
+ // Navigate to the nested rune
390
+ if (activeNode?.type === 'rune') {
391
+ activePath = [...activePath, treeIndex];
392
+ } else {
393
+ activePath = [treeIndex];
394
+ }
395
+
396
+ // Switch to settings tab to show the nested rune's settings
397
+ activeTab = 'settings';
398
+ }
399
+
273
400
  // ── Edit handlers ────────────────────────────────────────────
274
401
 
275
402
  function handleHeadingTextChange(text: string) {
@@ -558,7 +685,10 @@
558
685
  onremovefield={handleRemoveField}
559
686
  onappenditem={handleAppendItem}
560
687
  onremovelistitem={handleRemoveListItem}
688
+ onreorderlistitem={handleReorderListItem}
561
689
  oneditfield={handleEditField}
690
+ oneditlistitem={handleEditListItem}
691
+ onnavigaterune={handleNavigateRune}
562
692
  onfieldselect={handleFieldSelect}
563
693
  {selectedField}
564
694
  />
@@ -732,6 +862,8 @@
732
862
  .edit-panel {
733
863
  display: flex;
734
864
  flex-direction: column;
865
+ flex: 1;
866
+ min-height: 0;
735
867
  }
736
868
 
737
869
  .edit-panel__top {
@@ -885,6 +1017,7 @@
885
1017
  .edit-panel__content-editor {
886
1018
  display: flex;
887
1019
  flex-direction: column;
1020
+ flex-shrink: 0;
888
1021
  overflow: hidden;
889
1022
  margin-left: calc(-1 * var(--ed-space-5));
890
1023
  margin-right: calc(-1 * var(--ed-space-5));
@@ -12,7 +12,7 @@
12
12
  type ParsedBlock,
13
13
  type RuneBlock,
14
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';
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
16
  import { stripInlineMarkdown } from '../editor/inline-markdown.js';
17
17
  import { editorState } from '../state/editor.svelte.js';
18
18
  import BlockCard from './BlockCard.svelte';
@@ -23,6 +23,8 @@
23
23
  import InlineEditPopover from './InlineEditPopover.svelte';
24
24
  import ActionEditPopover from './ActionEditPopover.svelte';
25
25
  import CodeEditPopover from './CodeEditPopover.svelte';
26
+ import ImageEditPopover from './ImageEditPopover.svelte';
27
+ import IconPickerPopover from './IconPickerPopover.svelte';
26
28
 
27
29
  interface Props {
28
30
  bodyContent: string;
@@ -456,6 +458,30 @@
456
458
  return;
457
459
  }
458
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
+
459
485
  // Default: inline text editing
460
486
  const mapping = findSectionMapping(rb.innerContent, info.dataName, info.text);
461
487
  if (!mapping) return;
@@ -580,10 +606,103 @@
580
606
  commandEdit = null;
581
607
  }
582
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
+
583
701
  // ── Field edit from Structure tab ──────────────────────────────
584
702
 
585
703
  function handleFieldEdit(dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) {
586
704
  if (activeIndex === null) return;
705
+ commandEdit = null;
587
706
  inlineEdit = {
588
707
  blockIndex: activeIndex,
589
708
  dataName,
@@ -593,6 +712,16 @@
593
712
  };
594
713
  }
595
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
+
596
725
  // Group runes by category for the insert menu
597
726
  let runesByCategory = $derived.by(() => {
598
727
  const map = new Map<string, RuneInfo[]>();
@@ -741,6 +870,7 @@
741
870
  onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null; handleRemoveBlock(idx); }}
742
871
  onclose={() => { activeIndex = null; anchorPoint = null; pendingRuneIndex = null; }}
743
872
  oneditfield={handleFieldEdit}
873
+ oneditcode={handleFieldCodeEdit}
744
874
  />
745
875
  {/key}
746
876
  {/if}
@@ -783,10 +913,30 @@
783
913
  code={commandEdit.mapping.code}
784
914
  language={commandEdit.mapping.language}
785
915
  onchange={handleCommandEditChange}
916
+ onlanguagechange={handleLanguageChange}
786
917
  onremove={handleCommandRemove}
787
918
  onclose={closeCommandEdit}
788
919
  />
789
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}
790
940
  </div>
791
941
 
792
942
 
@@ -1005,7 +1155,9 @@
1005
1155
  .block-editor__popover {
1006
1156
  position: fixed;
1007
1157
  width: 420px;
1008
- overflow-y: auto;
1158
+ overflow: hidden;
1159
+ display: flex;
1160
+ flex-direction: column;
1009
1161
  background: var(--ed-surface-0);
1010
1162
  border-radius: var(--ed-radius-lg);
1011
1163
  border: 1px solid var(--ed-border-default);