@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
@@ -0,0 +1,835 @@
1
+ <script lang="ts">
2
+ import type { ResolvedStructure, ResolvedField, ResolvedZone } from '../editor/content-model-resolver.js';
3
+ import type { ContentNode } from '../editor/block-parser.js';
4
+ import { splitListItems } from '../editor/block-parser.js';
5
+
6
+ interface PreviewItem {
7
+ text: string;
8
+ type: 'text' | 'rune';
9
+ /** Index within field.nodes — used for rune navigation and greedy field editing */
10
+ nodeIndex?: number;
11
+ }
12
+
13
+ interface Props {
14
+ structure: ResolvedStructure;
15
+ rootLabel: string;
16
+ onaddfield: (fieldName: string, zoneName?: string) => void;
17
+ onremovefield: (fieldName: string, zoneName?: string) => void;
18
+ onappenditem: (fieldName: string, zoneName?: string) => void;
19
+ onremovelistitem: (fieldName: string, itemIndex: number, zoneName?: string) => void;
20
+ onreorderlistitem: (fieldName: string, fromIndex: number, toIndex: number, zoneName?: string) => void;
21
+ oneditfield: (fieldName: string, rect: DOMRect, zoneName?: string, nodeIndex?: number) => void;
22
+ oneditlistitem: (fieldName: string, itemIndex: number, rect: DOMRect, zoneName?: string) => void;
23
+ onnavigaterune: (fieldName: string, nodeIndex: number, zoneName?: string) => void;
24
+ onfieldselect: (fieldName: string, zoneName?: string) => void;
25
+ selectedField?: string | null;
26
+ }
27
+
28
+ let {
29
+ structure,
30
+ rootLabel,
31
+ onaddfield,
32
+ onremovefield,
33
+ onappenditem,
34
+ onremovelistitem,
35
+ onreorderlistitem,
36
+ oneditfield,
37
+ oneditlistitem,
38
+ onnavigaterune,
39
+ onfieldselect,
40
+ selectedField = null,
41
+ }: Props = $props();
42
+
43
+ // ── Drag and drop state ──────────────────────────────────────
44
+ let dragFieldName: string | null = $state(null);
45
+ let dragZoneName: string | undefined = $state(undefined);
46
+ let dragFromIndex: number | null = $state(null);
47
+ let dragOverIndex: number | null = $state(null);
48
+
49
+ function handleItemDragStart(e: DragEvent, fieldName: string, index: number, zoneName?: string) {
50
+ dragFieldName = fieldName;
51
+ dragZoneName = zoneName;
52
+ dragFromIndex = index;
53
+ dragOverIndex = index;
54
+ if (e.dataTransfer) {
55
+ e.dataTransfer.effectAllowed = 'move';
56
+ e.dataTransfer.setData('text/plain', String(index));
57
+ }
58
+ }
59
+
60
+ function handleItemDragOver(e: DragEvent, index: number) {
61
+ if (dragFromIndex === null) return;
62
+ e.preventDefault();
63
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
64
+ dragOverIndex = index;
65
+ }
66
+
67
+ function handleItemDrop(e: DragEvent, index: number) {
68
+ e.preventDefault();
69
+ if (dragFromIndex !== null && dragFromIndex !== index && dragFieldName !== null) {
70
+ onreorderlistitem(dragFieldName, dragFromIndex, index, dragZoneName);
71
+ }
72
+ dragFieldName = null;
73
+ dragZoneName = undefined;
74
+ dragFromIndex = null;
75
+ dragOverIndex = null;
76
+ }
77
+
78
+ function handleItemDragEnd() {
79
+ dragFieldName = null;
80
+ dragZoneName = undefined;
81
+ dragFromIndex = null;
82
+ dragOverIndex = null;
83
+ }
84
+
85
+ function isListField(match: string): boolean {
86
+ return match === 'list' || match.startsWith('list:');
87
+ }
88
+
89
+ function isGreedyItemField(field: ResolvedField): boolean {
90
+ return field.greedy && field.filled && field.match !== 'any';
91
+ }
92
+
93
+ /** Extract individual items from a greedy field for per-item rendering */
94
+ function greedyItemPreviews(field: ResolvedField): { text: string; index: number }[] {
95
+ return field.nodes.map((node, i) => {
96
+ let text: string;
97
+ if (node.type === 'fence') {
98
+ text = node.fenceLanguage ? `[${node.fenceLanguage}]` : '[code]';
99
+ } else if (node.type === 'heading' && node.headingText) {
100
+ text = node.headingText;
101
+ } else if (node.type === 'rune' && node.runeName) {
102
+ text = node.runeName;
103
+ } else if (node.type === 'image') {
104
+ text = '[image]';
105
+ } else {
106
+ const raw = node.source.replace(/\n/g, ' ').trim();
107
+ text = raw.length > 50 ? raw.slice(0, 47) + '...' : raw;
108
+ }
109
+ return { text: text.length > 50 ? text.slice(0, 47) + '...' : text, index: i };
110
+ });
111
+ }
112
+
113
+ /** Icon SVG path for a match type */
114
+ function matchIcon(match: string): string {
115
+ // Take first alternative for pipe-separated matches
116
+ const primary = match.includes('|') ? match.split('|')[0] : match;
117
+ switch (primary) {
118
+ case 'heading':
119
+ case 'heading:1': case 'heading:2': case 'heading:3':
120
+ case 'heading:4': case 'heading:5': case 'heading:6':
121
+ return 'M3 3v10M13 3v10M3 8h10';
122
+ case 'paragraph':
123
+ return 'M2 4h12M2 8h12M2 12h8';
124
+ case 'fence':
125
+ return 'M5 4L2 8l3 4M11 4l3 4-3 4';
126
+ case 'list': case 'list:ordered': case 'list:unordered':
127
+ return 'M4 4h10M4 8h10M4 12h10M2 4h0M2 8h0M2 12h0';
128
+ case 'blockquote': case 'quote':
129
+ return 'M4 4h8M4 7h6M1 3v8';
130
+ case 'image':
131
+ return 'M2 2h12v12H2zM5 5l-3 7h12l-4-5-2 2.5';
132
+ case 'any':
133
+ return 'M2 8h12M8 2v12';
134
+ default:
135
+ return 'M2 4h12M2 8h12M2 12h8';
136
+ }
137
+ }
138
+
139
+ /** Extract individual list items with their indices for per-item rendering */
140
+ function listItemPreviews(nodes: ContentNode[]): { text: string; index: number }[] {
141
+ const result: { text: string; index: number }[] = [];
142
+ for (const node of nodes) {
143
+ if (node.type !== 'list') continue;
144
+ const items = splitListItems(node.source);
145
+ for (let i = 0; i < items.length; i++) {
146
+ const firstLine = items[i].split('\n')[0];
147
+ const text = firstLine.replace(/^[-*+]\s+|^\d+\.\s+/, '').trim();
148
+ if (text) {
149
+ result.push({ text: text.length > 50 ? text.slice(0, 47) + '...' : text, index: i });
150
+ }
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+
156
+ /** Generate preview items for matched content */
157
+ function contentPreview(nodes: ContentNode[]): PreviewItem[] {
158
+ const previews: PreviewItem[] = [];
159
+ for (let idx = 0; idx < nodes.length; idx++) {
160
+ const node = nodes[idx];
161
+ if (node.type === 'heading' && node.headingText) {
162
+ previews.push({ text: node.headingText, type: 'text', nodeIndex: idx });
163
+ } else if (node.type === 'paragraph') {
164
+ const text = node.source.replace(/\n/g, ' ').trim();
165
+ previews.push({ text: text.length > 60 ? text.slice(0, 57) + '...' : text, type: 'text', nodeIndex: idx });
166
+ } else if (node.type === 'rune' && node.runeName) {
167
+ previews.push({ text: node.runeName, type: 'rune', nodeIndex: idx });
168
+ } else if (node.type === 'fence') {
169
+ const lang = node.fenceLanguage ? `[${node.fenceLanguage}]` : '[code]';
170
+ previews.push({ text: lang, type: 'text', nodeIndex: idx });
171
+ } else if (node.type === 'list') {
172
+ // Show abbreviated list items from source
173
+ const items = node.source.split('\n')
174
+ .filter(l => l.match(/^[-*\d.]\s/))
175
+ .map(l => l.replace(/^[-*\d.]+\s+/, '').trim())
176
+ .filter(Boolean);
177
+ for (const item of items.slice(0, 3)) {
178
+ previews.push({ text: item.length > 50 ? item.slice(0, 47) + '...' : item, type: 'text' });
179
+ }
180
+ if (items.length > 3) previews.push({ text: `... +${items.length - 3} more`, type: 'text' });
181
+ } else if (node.type === 'image') {
182
+ previews.push({ text: '[image]', type: 'text', nodeIndex: idx });
183
+ } else {
184
+ const text = node.source.replace(/\n/g, ' ').trim();
185
+ if (text) previews.push({ text: text.length > 50 ? text.slice(0, 47) + '...' : text, type: 'text', nodeIndex: idx });
186
+ }
187
+ }
188
+ return previews;
189
+ }
190
+ </script>
191
+
192
+ {#snippet previewItems(previews: PreviewItem[], fieldName: string, zoneName?: string)}
193
+ {#each previews as preview}
194
+ {#if preview.type === 'rune' && preview.nodeIndex !== undefined}
195
+ <button
196
+ type="button"
197
+ class="cm-tree__preview-line cm-tree__preview-line--rune"
198
+ onclick={() => onnavigaterune(fieldName, preview.nodeIndex!, zoneName)}
199
+ >
200
+ <span class="cm-tree__rune-dot"></span>
201
+ {preview.text}
202
+ </button>
203
+ {:else}
204
+ <button
205
+ type="button"
206
+ class="cm-tree__preview-line cm-tree__preview-line--clickable"
207
+ onclick={(e) => oneditfield(fieldName, (e.currentTarget as HTMLElement).getBoundingClientRect(), zoneName, preview.nodeIndex)}
208
+ >
209
+ {preview.text}
210
+ </button>
211
+ {/if}
212
+ {/each}
213
+ {/snippet}
214
+
215
+ {#snippet listItemRows(listItems: { text: string; index: number }[], fieldName: string, zoneName?: string)}
216
+ {#each listItems as item}
217
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
218
+ <div
219
+ class="cm-tree__preview-item"
220
+ class:cm-tree__preview-item--drag-over={dragFieldName === fieldName && dragZoneName === zoneName && dragOverIndex === item.index && dragFromIndex !== item.index}
221
+ class:cm-tree__preview-item--dragging={dragFieldName === fieldName && dragZoneName === zoneName && dragFromIndex === item.index}
222
+ draggable="true"
223
+ ondragstart={(e) => handleItemDragStart(e, fieldName, item.index, zoneName)}
224
+ ondragover={(e) => handleItemDragOver(e, item.index)}
225
+ ondrop={(e) => handleItemDrop(e, item.index)}
226
+ ondragend={handleItemDragEnd}
227
+ >
228
+ <span
229
+ class="cm-tree__preview-grip"
230
+ title="Drag to reorder"
231
+ >
232
+ <svg width="6" height="10" viewBox="0 0 6 10" fill="none">
233
+ <circle cx="1.5" cy="1.5" r="1" fill="currentColor"/>
234
+ <circle cx="4.5" cy="1.5" r="1" fill="currentColor"/>
235
+ <circle cx="1.5" cy="5" r="1" fill="currentColor"/>
236
+ <circle cx="4.5" cy="5" r="1" fill="currentColor"/>
237
+ <circle cx="1.5" cy="8.5" r="1" fill="currentColor"/>
238
+ <circle cx="4.5" cy="8.5" r="1" fill="currentColor"/>
239
+ </svg>
240
+ </span>
241
+ <button
242
+ type="button"
243
+ class="cm-tree__preview-text cm-tree__preview-text--clickable"
244
+ onclick={(e) => {
245
+ e.stopPropagation();
246
+ oneditlistitem(fieldName, item.index, (e.currentTarget as HTMLElement).getBoundingClientRect(), zoneName);
247
+ }}
248
+ >
249
+ {item.text}
250
+ </button>
251
+ <button
252
+ type="button"
253
+ class="cm-tree__preview-remove"
254
+ title="Remove item"
255
+ onclick={() => onremovelistitem(fieldName, item.index, zoneName)}
256
+ >
257
+ <svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
258
+ <line x1="4" y1="4" x2="12" y2="12" />
259
+ <line x1="12" y1="4" x2="4" y2="12" />
260
+ </svg>
261
+ </button>
262
+ </div>
263
+ {/each}
264
+ {/snippet}
265
+
266
+ {#snippet greedyItemRows(items: { text: string; index: number }[], fieldName: string, zoneName?: string)}
267
+ {#each items as item}
268
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
269
+ <div
270
+ class="cm-tree__preview-item"
271
+ class:cm-tree__preview-item--drag-over={dragFieldName === fieldName && dragZoneName === zoneName && dragOverIndex === item.index && dragFromIndex !== item.index}
272
+ class:cm-tree__preview-item--dragging={dragFieldName === fieldName && dragZoneName === zoneName && dragFromIndex === item.index}
273
+ draggable="true"
274
+ ondragstart={(e) => handleItemDragStart(e, fieldName, item.index, zoneName)}
275
+ ondragover={(e) => handleItemDragOver(e, item.index)}
276
+ ondrop={(e) => handleItemDrop(e, item.index)}
277
+ ondragend={handleItemDragEnd}
278
+ >
279
+ <span
280
+ class="cm-tree__preview-grip"
281
+ title="Drag to reorder"
282
+ >
283
+ <svg width="6" height="10" viewBox="0 0 6 10" fill="none">
284
+ <circle cx="1.5" cy="1.5" r="1" fill="currentColor"/>
285
+ <circle cx="4.5" cy="1.5" r="1" fill="currentColor"/>
286
+ <circle cx="1.5" cy="5" r="1" fill="currentColor"/>
287
+ <circle cx="4.5" cy="5" r="1" fill="currentColor"/>
288
+ <circle cx="1.5" cy="8.5" r="1" fill="currentColor"/>
289
+ <circle cx="4.5" cy="8.5" r="1" fill="currentColor"/>
290
+ </svg>
291
+ </span>
292
+ <button
293
+ type="button"
294
+ class="cm-tree__preview-text cm-tree__preview-text--clickable"
295
+ onclick={(e) => {
296
+ e.stopPropagation();
297
+ oneditfield(fieldName, (e.currentTarget as HTMLElement).getBoundingClientRect(), zoneName, item.index);
298
+ }}
299
+ >
300
+ {item.text}
301
+ </button>
302
+ <button
303
+ type="button"
304
+ class="cm-tree__preview-remove"
305
+ title="Remove item"
306
+ onclick={() => onremovelistitem(fieldName, item.index, zoneName)}
307
+ >
308
+ <svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
309
+ <line x1="4" y1="4" x2="12" y2="12" />
310
+ <line x1="12" y1="4" x2="4" y2="12" />
311
+ </svg>
312
+ </button>
313
+ </div>
314
+ {/each}
315
+ {/snippet}
316
+
317
+ <div class="cm-tree">
318
+ <!-- Root node -->
319
+ <div class="cm-tree__root">
320
+ <span class="cm-tree__rune-dot"></span>
321
+ <span class="cm-tree__root-label">{rootLabel}</span>
322
+ </div>
323
+
324
+ {#if structure.type === 'sequence'}
325
+ <div class="cm-tree__fields">
326
+ {#each structure.fields as field}
327
+ {@const previews = contentPreview(field.nodes)}
328
+ {@const listItems = isListField(field.match) ? listItemPreviews(field.nodes) : []}
329
+ {@const greedyItems = isGreedyItemField(field) ? greedyItemPreviews(field) : []}
330
+ <button
331
+ type="button"
332
+ class="cm-tree__field"
333
+ class:cm-tree__field--filled={field.filled}
334
+ class:cm-tree__field--empty={!field.filled}
335
+ class:cm-tree__field--required={!field.optional && !field.filled}
336
+ class:cm-tree__field--selected={selectedField === field.name}
337
+ onclick={() => onfieldselect(field.name)}
338
+ >
339
+ <svg class="cm-tree__field-icon" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
340
+ <path d={matchIcon(field.match)} />
341
+ </svg>
342
+ <span class="cm-tree__field-name">{field.name}</span>
343
+ {#if !field.optional && !field.filled}
344
+ <span class="cm-tree__required-badge">required</span>
345
+ {/if}
346
+ <span class="cm-tree__field-spacer"></span>
347
+ {#if field.filled}
348
+ {#if isListField(field.match) || isGreedyItemField(field)}
349
+ <button
350
+ type="button"
351
+ class="cm-tree__action cm-tree__action--add"
352
+ title="Add item to {field.name}"
353
+ onclick={(e) => { e.stopPropagation(); onappenditem(field.name); }}
354
+ >
355
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
356
+ <line x1="8" y1="3" x2="8" y2="13" />
357
+ <line x1="3" y1="8" x2="13" y2="8" />
358
+ </svg>
359
+ </button>
360
+ {/if}
361
+ <button
362
+ type="button"
363
+ class="cm-tree__action cm-tree__action--remove"
364
+ title="Remove {field.name}"
365
+ onclick={(e) => { e.stopPropagation(); onremovefield(field.name); }}
366
+ >
367
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
368
+ <polyline points="3 6 3 14 13 14 13 6" />
369
+ <line x1="1" y1="4" x2="15" y2="4" />
370
+ <line x1="6" y1="8" x2="6" y2="12" />
371
+ <line x1="10" y1="8" x2="10" y2="12" />
372
+ </svg>
373
+ </button>
374
+ {:else}
375
+ <button
376
+ type="button"
377
+ class="cm-tree__action cm-tree__action--add"
378
+ title="Add {field.name}"
379
+ onclick={(e) => { e.stopPropagation(); onaddfield(field.name); }}
380
+ >
381
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
382
+ <line x1="8" y1="3" x2="8" y2="13" />
383
+ <line x1="3" y1="8" x2="13" y2="8" />
384
+ </svg>
385
+ </button>
386
+ {/if}
387
+ </button>
388
+ {#if field.filled && isListField(field.match) && listItems.length > 0}
389
+ <div class="cm-tree__previews">
390
+ {@render listItemRows(listItems, field.name)}
391
+ </div>
392
+ {:else if field.filled && greedyItems.length > 0}
393
+ <div class="cm-tree__previews">
394
+ {@render greedyItemRows(greedyItems, field.name)}
395
+ </div>
396
+ {:else if field.filled && previews.length > 0}
397
+ <div class="cm-tree__previews">
398
+ {@render previewItems(previews, field.name)}
399
+ </div>
400
+ {/if}
401
+ {/each}
402
+ </div>
403
+
404
+ {:else if structure.type === 'delimited'}
405
+ {#each structure.zones as zone, zoneIdx}
406
+ {#if zoneIdx > 0}
407
+ <div class="cm-tree__delimiter">
408
+ <span class="cm-tree__delimiter-line"></span>
409
+ </div>
410
+ {/if}
411
+ <div class="cm-tree__zone">
412
+ <div class="cm-tree__zone-label">{zone.name}</div>
413
+ <div class="cm-tree__fields">
414
+ {#each zone.fields as field}
415
+ {@const previews = contentPreview(field.nodes)}
416
+ {@const listItems = isListField(field.match) ? listItemPreviews(field.nodes) : []}
417
+ {@const greedyItems = isGreedyItemField(field) ? greedyItemPreviews(field) : []}
418
+ <button
419
+ type="button"
420
+ class="cm-tree__field"
421
+ class:cm-tree__field--filled={field.filled}
422
+ class:cm-tree__field--empty={!field.filled}
423
+ class:cm-tree__field--required={!field.optional && !field.filled}
424
+ class:cm-tree__field--selected={selectedField === `${zone.name}.${field.name}`}
425
+ onclick={() => onfieldselect(field.name, zone.name)}
426
+ >
427
+ <svg class="cm-tree__field-icon" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
428
+ <path d={matchIcon(field.match)} />
429
+ </svg>
430
+ <span class="cm-tree__field-name">{field.name}</span>
431
+ {#if !field.optional && !field.filled}
432
+ <span class="cm-tree__required-badge">required</span>
433
+ {/if}
434
+ <span class="cm-tree__field-spacer"></span>
435
+ {#if field.filled}
436
+ {#if isListField(field.match) || isGreedyItemField(field)}
437
+ <button
438
+ type="button"
439
+ class="cm-tree__action cm-tree__action--add"
440
+ title="Add item to {field.name}"
441
+ onclick={(e) => { e.stopPropagation(); onappenditem(field.name, zone.name); }}
442
+ >
443
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
444
+ <line x1="8" y1="3" x2="8" y2="13" />
445
+ <line x1="3" y1="8" x2="13" y2="8" />
446
+ </svg>
447
+ </button>
448
+ {/if}
449
+ <button
450
+ type="button"
451
+ class="cm-tree__action cm-tree__action--remove"
452
+ title="Remove {field.name}"
453
+ onclick={(e) => { e.stopPropagation(); onremovefield(field.name, zone.name); }}
454
+ >
455
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
456
+ <polyline points="3 6 3 14 13 14 13 6" />
457
+ <line x1="1" y1="4" x2="15" y2="4" />
458
+ <line x1="6" y1="8" x2="6" y2="12" />
459
+ <line x1="10" y1="8" x2="10" y2="12" />
460
+ </svg>
461
+ </button>
462
+ {:else}
463
+ <button
464
+ type="button"
465
+ class="cm-tree__action cm-tree__action--add"
466
+ title="Add {field.name}"
467
+ onclick={(e) => { e.stopPropagation(); onaddfield(field.name, zone.name); }}
468
+ >
469
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
470
+ <line x1="8" y1="3" x2="8" y2="13" />
471
+ <line x1="3" y1="8" x2="13" y2="8" />
472
+ </svg>
473
+ </button>
474
+ {/if}
475
+ </button>
476
+ {#if field.filled && isListField(field.match) && listItems.length > 0}
477
+ <div class="cm-tree__previews">
478
+ {@render listItemRows(listItems, field.name, zone.name)}
479
+ </div>
480
+ {:else if field.filled && greedyItems.length > 0}
481
+ <div class="cm-tree__previews">
482
+ {@render greedyItemRows(greedyItems, field.name, zone.name)}
483
+ </div>
484
+ {:else if field.filled && previews.length > 0}
485
+ <div class="cm-tree__previews">
486
+ {@render previewItems(previews, field.name, zone.name)}
487
+ </div>
488
+ {/if}
489
+ {/each}
490
+ </div>
491
+ </div>
492
+ {/each}
493
+
494
+ {:else if structure.type === 'sections' || structure.type === 'custom'}
495
+ <div class="cm-tree__description">
496
+ {structure.description}
497
+ </div>
498
+ {/if}
499
+ </div>
500
+
501
+ <style>
502
+ .cm-tree {
503
+ display: flex;
504
+ flex-direction: column;
505
+ gap: 0.15rem;
506
+ }
507
+
508
+ .cm-tree__root {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 0.35rem;
512
+ padding: 0.25rem 0.4rem;
513
+ font-weight: 500;
514
+ color: var(--ed-text-primary);
515
+ font-size: var(--ed-text-base);
516
+ }
517
+
518
+ .cm-tree__rune-dot {
519
+ width: 5px;
520
+ height: 5px;
521
+ border-radius: 50%;
522
+ background: var(--ed-warning);
523
+ flex-shrink: 0;
524
+ }
525
+
526
+ .cm-tree__root-label {
527
+ font-weight: 600;
528
+ }
529
+
530
+ /* Zone */
531
+ .cm-tree__zone {
532
+ display: flex;
533
+ flex-direction: column;
534
+ gap: 0.15rem;
535
+ }
536
+
537
+ .cm-tree__zone-label {
538
+ padding: 0.2rem 0.4rem 0.15rem 1.2rem;
539
+ font-size: var(--ed-text-xs);
540
+ font-weight: 600;
541
+ color: var(--ed-text-tertiary);
542
+ text-transform: uppercase;
543
+ letter-spacing: 0.04em;
544
+ }
545
+
546
+ .cm-tree__delimiter {
547
+ display: flex;
548
+ align-items: center;
549
+ padding: 0.3rem 1.2rem;
550
+ }
551
+
552
+ .cm-tree__delimiter-line {
553
+ flex: 1;
554
+ height: 1px;
555
+ background: var(--ed-border-subtle);
556
+ }
557
+
558
+ /* Fields */
559
+ .cm-tree__fields {
560
+ display: flex;
561
+ flex-direction: column;
562
+ gap: 0.1rem;
563
+ }
564
+
565
+ .cm-tree__field {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 0.35rem;
569
+ width: 100%;
570
+ padding: 0.25rem 0.4rem 0.25rem 1.6rem;
571
+ border: none;
572
+ border-radius: calc(var(--ed-radius-sm, 4px) - 1px);
573
+ background: transparent;
574
+ color: var(--ed-text-secondary);
575
+ font-size: var(--ed-text-sm);
576
+ cursor: pointer;
577
+ text-align: left;
578
+ transition: background var(--ed-transition-fast);
579
+ }
580
+
581
+ .cm-tree__field:hover {
582
+ background: var(--ed-surface-2);
583
+ }
584
+
585
+ .cm-tree__field--selected {
586
+ background: var(--ed-accent-muted);
587
+ color: var(--ed-accent);
588
+ }
589
+
590
+ .cm-tree__field--empty {
591
+ color: var(--ed-text-muted);
592
+ }
593
+
594
+ .cm-tree__field--required {
595
+ color: var(--ed-warning-text);
596
+ }
597
+
598
+ .cm-tree__field-icon {
599
+ flex-shrink: 0;
600
+ opacity: 0.6;
601
+ }
602
+
603
+ .cm-tree__field--empty .cm-tree__field-icon {
604
+ opacity: 0.3;
605
+ }
606
+
607
+ .cm-tree__field-name {
608
+ font-weight: 500;
609
+ white-space: nowrap;
610
+ }
611
+
612
+ .cm-tree__field--filled .cm-tree__field-name {
613
+ color: var(--ed-text-primary);
614
+ }
615
+
616
+ .cm-tree__required-badge {
617
+ font-size: 9px;
618
+ font-weight: 600;
619
+ padding: 0.05rem 0.3rem;
620
+ border-radius: 99px;
621
+ background: var(--ed-warning-subtle);
622
+ color: var(--ed-warning-text);
623
+ white-space: nowrap;
624
+ line-height: 1.2;
625
+ }
626
+
627
+ .cm-tree__field-spacer {
628
+ flex: 1;
629
+ }
630
+
631
+ /* Action buttons */
632
+ .cm-tree__action {
633
+ display: flex;
634
+ align-items: center;
635
+ justify-content: center;
636
+ width: 20px;
637
+ height: 20px;
638
+ padding: 0;
639
+ border: none;
640
+ border-radius: var(--ed-radius-sm);
641
+ background: transparent;
642
+ color: var(--ed-text-muted);
643
+ cursor: pointer;
644
+ flex-shrink: 0;
645
+ opacity: 0;
646
+ transition: opacity var(--ed-transition-fast), color var(--ed-transition-fast), background var(--ed-transition-fast);
647
+ }
648
+
649
+ .cm-tree__field:hover .cm-tree__action {
650
+ opacity: 1;
651
+ }
652
+
653
+ .cm-tree__action--add:hover {
654
+ color: var(--ed-accent);
655
+ background: var(--ed-accent-muted);
656
+ }
657
+
658
+ .cm-tree__action--remove:hover {
659
+ color: var(--ed-danger);
660
+ background: var(--ed-danger-subtle);
661
+ }
662
+
663
+ /* Content previews */
664
+ .cm-tree__previews {
665
+ display: flex;
666
+ flex-direction: column;
667
+ gap: 0.05rem;
668
+ padding-left: 2.8rem;
669
+ padding-bottom: 0.15rem;
670
+ }
671
+
672
+ .cm-tree__preview-line {
673
+ font-size: var(--ed-text-xs);
674
+ color: var(--ed-text-muted);
675
+ overflow: hidden;
676
+ text-overflow: ellipsis;
677
+ white-space: nowrap;
678
+ line-height: 1.4;
679
+ }
680
+
681
+ .cm-tree__preview-line--clickable {
682
+ display: block;
683
+ width: 100%;
684
+ background: none;
685
+ border: none;
686
+ padding: 0.05rem 0.25rem;
687
+ cursor: pointer;
688
+ text-align: left;
689
+ font: inherit;
690
+ font-size: var(--ed-text-xs);
691
+ color: var(--ed-text-muted);
692
+ border-radius: 2px;
693
+ overflow: hidden;
694
+ text-overflow: ellipsis;
695
+ white-space: nowrap;
696
+ line-height: 1.4;
697
+ transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
698
+ }
699
+
700
+ .cm-tree__preview-line--clickable:hover {
701
+ background: var(--ed-surface-2);
702
+ color: var(--ed-text-secondary);
703
+ }
704
+
705
+ .cm-tree__preview-line--rune {
706
+ display: flex;
707
+ align-items: center;
708
+ gap: 0.3rem;
709
+ width: 100%;
710
+ background: none;
711
+ border: none;
712
+ padding: 0.05rem 0.25rem;
713
+ cursor: pointer;
714
+ text-align: left;
715
+ font: inherit;
716
+ font-size: var(--ed-text-xs);
717
+ color: var(--ed-warning-text);
718
+ font-weight: 500;
719
+ border-radius: 2px;
720
+ line-height: 1.4;
721
+ transition: background var(--ed-transition-fast);
722
+ }
723
+
724
+ .cm-tree__preview-line--rune:hover {
725
+ background: var(--ed-warning-subtle);
726
+ }
727
+
728
+ .cm-tree__preview-item {
729
+ display: flex;
730
+ align-items: center;
731
+ gap: 0.25rem;
732
+ line-height: 1.4;
733
+ border-top: 2px solid transparent;
734
+ transition: border-color var(--ed-transition-fast);
735
+ }
736
+
737
+ .cm-tree__preview-item--drag-over {
738
+ border-top-color: var(--ed-accent);
739
+ }
740
+
741
+ .cm-tree__preview-item--dragging {
742
+ opacity: 0.4;
743
+ }
744
+
745
+ .cm-tree__preview-text {
746
+ font-size: var(--ed-text-xs);
747
+ color: var(--ed-text-muted);
748
+ overflow: hidden;
749
+ text-overflow: ellipsis;
750
+ white-space: nowrap;
751
+ flex: 1;
752
+ min-width: 0;
753
+ }
754
+
755
+ .cm-tree__preview-text--clickable {
756
+ background: none;
757
+ border: none;
758
+ padding: 0.05rem 0.15rem;
759
+ cursor: pointer;
760
+ font: inherit;
761
+ font-size: var(--ed-text-xs);
762
+ color: var(--ed-text-muted);
763
+ text-align: left;
764
+ border-radius: 2px;
765
+ overflow: hidden;
766
+ text-overflow: ellipsis;
767
+ white-space: nowrap;
768
+ flex: 1;
769
+ min-width: 0;
770
+ transition: color var(--ed-transition-fast), background var(--ed-transition-fast);
771
+ }
772
+
773
+ .cm-tree__preview-text--clickable:hover {
774
+ color: var(--ed-text-secondary);
775
+ background: var(--ed-surface-2);
776
+ }
777
+
778
+ /* Drag grip handle */
779
+ .cm-tree__preview-grip {
780
+ display: flex;
781
+ align-items: center;
782
+ justify-content: center;
783
+ width: 12px;
784
+ height: 16px;
785
+ padding: 0;
786
+ color: var(--ed-text-muted);
787
+ cursor: grab;
788
+ flex-shrink: 0;
789
+ opacity: 0;
790
+ transition: opacity var(--ed-transition-fast), color var(--ed-transition-fast);
791
+ }
792
+
793
+ .cm-tree__preview-item:hover .cm-tree__preview-grip {
794
+ opacity: 0.6;
795
+ }
796
+
797
+ .cm-tree__preview-grip:hover {
798
+ opacity: 1 !important;
799
+ color: var(--ed-text-secondary);
800
+ }
801
+
802
+ .cm-tree__preview-remove {
803
+ display: flex;
804
+ align-items: center;
805
+ justify-content: center;
806
+ width: 16px;
807
+ height: 16px;
808
+ padding: 0;
809
+ border: none;
810
+ border-radius: var(--ed-radius-sm);
811
+ background: transparent;
812
+ color: var(--ed-text-muted);
813
+ cursor: pointer;
814
+ flex-shrink: 0;
815
+ opacity: 0;
816
+ transition: opacity var(--ed-transition-fast), color var(--ed-transition-fast), background var(--ed-transition-fast);
817
+ }
818
+
819
+ .cm-tree__preview-item:hover .cm-tree__preview-remove {
820
+ opacity: 1;
821
+ }
822
+
823
+ .cm-tree__preview-remove:hover {
824
+ color: var(--ed-danger);
825
+ background: var(--ed-danger-subtle);
826
+ }
827
+
828
+ /* Description fallback for sections/custom */
829
+ .cm-tree__description {
830
+ padding: 0.5rem 1.2rem;
831
+ font-size: var(--ed-text-sm);
832
+ color: var(--ed-text-muted);
833
+ font-style: italic;
834
+ }
835
+ </style>