@refrakt-md/editor 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/app/dist/assets/{index-D3TQo8gu.js → index-3MvwKRVQ.js} +1 -1
  2. package/app/dist/assets/{index-CeU_s7BB.js → index-B7e694w6.js} +1 -1
  3. package/app/dist/assets/{index-DzHt8ZRh.js → index-BBljOYQu.js} +1 -1
  4. package/app/dist/assets/{index-C72UC2ga.js → index-BEGy_i8o.js} +1 -1
  5. package/app/dist/assets/{index-CqHjo2YT.js → index-BGy7ixjW.js} +1 -1
  6. package/app/dist/assets/{index-DVM3uoxc.js → index-BaLgiiKk.js} +1 -1
  7. package/app/dist/assets/{index-CW02bulk.js → index-BjlNcvOf.js} +1 -1
  8. package/app/dist/assets/{index-DmY6uqAw.js → index-CKfKYVw7.js} +1 -1
  9. package/app/dist/assets/{index-BLuaHLN3.js → index-COFbngzR.js} +1 -1
  10. package/app/dist/assets/{index-BBinZAiy.js → index-CPEo_rvd.js} +1 -1
  11. package/app/dist/assets/{index-D_Y6J00B.js → index-CQDCT-XT.js} +1 -1
  12. package/app/dist/assets/{index-COIPZ34u.js → index-CUmEjEeR.js} +1 -1
  13. package/app/dist/assets/{index-BgCNqcSo.js → index-CeV-Af4N.js} +1 -1
  14. package/app/dist/assets/{index-DW2zI-Ss.js → index-ChbH55h5.js} +1 -1
  15. package/app/dist/assets/index-CzvG5PZT.css +1 -0
  16. package/app/dist/assets/{index-ZLvRNfLb.js → index-D9-aYc3I.js} +1 -1
  17. package/app/dist/assets/{index-BwFn9q4x.js → index-DezxtfNV.js} +1 -1
  18. package/app/dist/assets/{index-CXFMPmtf.js → index-DrI4IfXE.js} +1 -1
  19. package/app/dist/assets/{index-DgIg-QAA.js → index-DwfxgjnU.js} +2 -2
  20. package/app/dist/assets/index-ogrpJNou.js +555 -0
  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 +534 -48
  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/InlineEditor.svelte +15 -5
  33. package/app/src/lib/components/ProseBlockCard.svelte +446 -0
  34. package/app/src/lib/components/ProseEditPanel.svelte +470 -0
  35. package/app/src/lib/components/RuneAttributes.svelte +51 -0
  36. package/app/src/lib/editor/block-parser.ts +211 -9
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +129 -1
  39. package/dist/server.js.map +1 -1
  40. package/package.json +6 -6
  41. package/app/dist/assets/index-BD2EBUrQ.css +0 -1
  42. package/app/dist/assets/index-BlAOhWAQ.js +0 -453
@@ -3,6 +3,13 @@
3
3
  import type { ContentNode } from '../editor/block-parser.js';
4
4
  import { splitListItems } from '../editor/block-parser.js';
5
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
+
6
13
  interface Props {
7
14
  structure: ResolvedStructure;
8
15
  rootLabel: string;
@@ -10,7 +17,10 @@
10
17
  onremovefield: (fieldName: string, zoneName?: string) => void;
11
18
  onappenditem: (fieldName: string, zoneName?: string) => void;
12
19
  onremovelistitem: (fieldName: string, itemIndex: number, zoneName?: string) => void;
13
- oneditfield: (fieldName: string, rect: DOMRect, 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;
14
24
  onfieldselect: (fieldName: string, zoneName?: string) => void;
15
25
  selectedField?: string | null;
16
26
  }
@@ -22,15 +32,84 @@
22
32
  onremovefield,
23
33
  onappenditem,
24
34
  onremovelistitem,
35
+ onreorderlistitem,
25
36
  oneditfield,
37
+ oneditlistitem,
38
+ onnavigaterune,
26
39
  onfieldselect,
27
40
  selectedField = null,
28
41
  }: Props = $props();
29
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
+
30
85
  function isListField(match: string): boolean {
31
86
  return match === 'list' || match.startsWith('list:');
32
87
  }
33
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
+
34
113
  /** Icon SVG path for a match type */
35
114
  function matchIcon(match: string): string {
36
115
  // Take first alternative for pipe-separated matches
@@ -74,20 +153,21 @@
74
153
  return result;
75
154
  }
76
155
 
77
- /** Generate a preview string for matched content */
78
- function contentPreview(nodes: ContentNode[]): string[] {
79
- const previews: string[] = [];
80
- for (const node of nodes) {
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];
81
161
  if (node.type === 'heading' && node.headingText) {
82
- previews.push(node.headingText);
162
+ previews.push({ text: node.headingText, type: 'text', nodeIndex: idx });
83
163
  } else if (node.type === 'paragraph') {
84
164
  const text = node.source.replace(/\n/g, ' ').trim();
85
- previews.push(text.length > 60 ? text.slice(0, 57) + '...' : text);
165
+ previews.push({ text: text.length > 60 ? text.slice(0, 57) + '...' : text, type: 'text', nodeIndex: idx });
86
166
  } else if (node.type === 'rune' && node.runeName) {
87
- previews.push(node.runeName);
167
+ previews.push({ text: node.runeName, type: 'rune', nodeIndex: idx });
88
168
  } else if (node.type === 'fence') {
89
169
  const lang = node.fenceLanguage ? `[${node.fenceLanguage}]` : '[code]';
90
- previews.push(lang);
170
+ previews.push({ text: lang, type: 'text', nodeIndex: idx });
91
171
  } else if (node.type === 'list') {
92
172
  // Show abbreviated list items from source
93
173
  const items = node.source.split('\n')
@@ -95,20 +175,145 @@
95
175
  .map(l => l.replace(/^[-*\d.]+\s+/, '').trim())
96
176
  .filter(Boolean);
97
177
  for (const item of items.slice(0, 3)) {
98
- previews.push(item.length > 50 ? item.slice(0, 47) + '...' : item);
178
+ previews.push({ text: item.length > 50 ? item.slice(0, 47) + '...' : item, type: 'text' });
99
179
  }
100
- if (items.length > 3) previews.push(`... +${items.length - 3} more`);
180
+ if (items.length > 3) previews.push({ text: `... +${items.length - 3} more`, type: 'text' });
101
181
  } else if (node.type === 'image') {
102
- previews.push('[image]');
182
+ previews.push({ text: '[image]', type: 'text', nodeIndex: idx });
103
183
  } else {
104
184
  const text = node.source.replace(/\n/g, ' ').trim();
105
- if (text) previews.push(text.length > 50 ? text.slice(0, 47) + '...' : text);
185
+ if (text) previews.push({ text: text.length > 50 ? text.slice(0, 47) + '...' : text, type: 'text', nodeIndex: idx });
106
186
  }
107
187
  }
108
188
  return previews;
109
189
  }
110
190
  </script>
111
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
+
112
317
  <div class="cm-tree">
113
318
  <!-- Root node -->
114
319
  <div class="cm-tree__root">
@@ -121,6 +326,7 @@
121
326
  {#each structure.fields as field}
122
327
  {@const previews = contentPreview(field.nodes)}
123
328
  {@const listItems = isListField(field.match) ? listItemPreviews(field.nodes) : []}
329
+ {@const greedyItems = isGreedyItemField(field) ? greedyItemPreviews(field) : []}
124
330
  <button
125
331
  type="button"
126
332
  class="cm-tree__field"
@@ -128,13 +334,7 @@
128
334
  class:cm-tree__field--empty={!field.filled}
129
335
  class:cm-tree__field--required={!field.optional && !field.filled}
130
336
  class:cm-tree__field--selected={selectedField === field.name}
131
- onclick={(e) => {
132
- if (field.filled && !isListField(field.match)) {
133
- oneditfield(field.name, (e.currentTarget as HTMLElement).getBoundingClientRect());
134
- } else {
135
- onfieldselect(field.name);
136
- }
137
- }}
337
+ onclick={() => onfieldselect(field.name)}
138
338
  >
139
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">
140
340
  <path d={matchIcon(field.match)} />
@@ -145,7 +345,7 @@
145
345
  {/if}
146
346
  <span class="cm-tree__field-spacer"></span>
147
347
  {#if field.filled}
148
- {#if isListField(field.match)}
348
+ {#if isListField(field.match) || isGreedyItemField(field)}
149
349
  <button
150
350
  type="button"
151
351
  class="cm-tree__action cm-tree__action--add"
@@ -187,28 +387,15 @@
187
387
  </button>
188
388
  {#if field.filled && isListField(field.match) && listItems.length > 0}
189
389
  <div class="cm-tree__previews">
190
- {#each listItems as item}
191
- <div class="cm-tree__preview-item">
192
- <span class="cm-tree__preview-text">{item.text}</span>
193
- <button
194
- type="button"
195
- class="cm-tree__preview-remove"
196
- title="Remove item"
197
- onclick={() => onremovelistitem(field.name, item.index)}
198
- >
199
- <svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
200
- <line x1="4" y1="4" x2="12" y2="12" />
201
- <line x1="12" y1="4" x2="4" y2="12" />
202
- </svg>
203
- </button>
204
- </div>
205
- {/each}
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)}
206
395
  </div>
207
396
  {:else if field.filled && previews.length > 0}
208
397
  <div class="cm-tree__previews">
209
- {#each previews as preview}
210
- <div class="cm-tree__preview-line">{preview}</div>
211
- {/each}
398
+ {@render previewItems(previews, field.name)}
212
399
  </div>
213
400
  {/if}
214
401
  {/each}
@@ -227,6 +414,7 @@
227
414
  {#each zone.fields as field}
228
415
  {@const previews = contentPreview(field.nodes)}
229
416
  {@const listItems = isListField(field.match) ? listItemPreviews(field.nodes) : []}
417
+ {@const greedyItems = isGreedyItemField(field) ? greedyItemPreviews(field) : []}
230
418
  <button
231
419
  type="button"
232
420
  class="cm-tree__field"
@@ -234,13 +422,7 @@
234
422
  class:cm-tree__field--empty={!field.filled}
235
423
  class:cm-tree__field--required={!field.optional && !field.filled}
236
424
  class:cm-tree__field--selected={selectedField === `${zone.name}.${field.name}`}
237
- onclick={(e) => {
238
- if (field.filled && !isListField(field.match)) {
239
- oneditfield(field.name, (e.currentTarget as HTMLElement).getBoundingClientRect(), zone.name);
240
- } else {
241
- onfieldselect(field.name, zone.name);
242
- }
243
- }}
425
+ onclick={() => onfieldselect(field.name, zone.name)}
244
426
  >
245
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">
246
428
  <path d={matchIcon(field.match)} />
@@ -251,7 +433,7 @@
251
433
  {/if}
252
434
  <span class="cm-tree__field-spacer"></span>
253
435
  {#if field.filled}
254
- {#if isListField(field.match)}
436
+ {#if isListField(field.match) || isGreedyItemField(field)}
255
437
  <button
256
438
  type="button"
257
439
  class="cm-tree__action cm-tree__action--add"
@@ -293,28 +475,15 @@
293
475
  </button>
294
476
  {#if field.filled && isListField(field.match) && listItems.length > 0}
295
477
  <div class="cm-tree__previews">
296
- {#each listItems as item}
297
- <div class="cm-tree__preview-item">
298
- <span class="cm-tree__preview-text">{item.text}</span>
299
- <button
300
- type="button"
301
- class="cm-tree__preview-remove"
302
- title="Remove item"
303
- onclick={() => onremovelistitem(field.name, item.index, zone.name)}
304
- >
305
- <svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
306
- <line x1="4" y1="4" x2="12" y2="12" />
307
- <line x1="12" y1="4" x2="4" y2="12" />
308
- </svg>
309
- </button>
310
- </div>
311
- {/each}
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)}
312
483
  </div>
313
484
  {:else if field.filled && previews.length > 0}
314
485
  <div class="cm-tree__previews">
315
- {#each previews as preview}
316
- <div class="cm-tree__preview-line">{preview}</div>
317
- {/each}
486
+ {@render previewItems(previews, field.name, zone.name)}
318
487
  </div>
319
488
  {/if}
320
489
  {/each}
@@ -509,11 +678,68 @@
509
678
  line-height: 1.4;
510
679
  }
511
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
+
512
728
  .cm-tree__preview-item {
513
729
  display: flex;
514
730
  align-items: center;
515
731
  gap: 0.25rem;
516
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;
517
743
  }
518
744
 
519
745
  .cm-tree__preview-text {
@@ -526,6 +752,53 @@
526
752
  min-width: 0;
527
753
  }
528
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
+
529
802
  .cm-tree__preview-remove {
530
803
  display: flex;
531
804
  align-items: center;