@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
@@ -0,0 +1,470 @@
1
+ <script lang="ts">
2
+ import type { RuneInfo } from '../api/client.js';
3
+ import type {
4
+ ProseBlock,
5
+ ParsedBlock,
6
+ HeadingBlock,
7
+ FenceBlock,
8
+ ListBlock,
9
+ } from '../editor/block-parser.js';
10
+ import {
11
+ blockLabel,
12
+ parseBlocks,
13
+ serializeBlocks,
14
+ } from '../editor/block-parser.js';
15
+ import InlineEditor from './InlineEditor.svelte';
16
+
17
+ interface Props {
18
+ block: ProseBlock;
19
+ runes: () => RuneInfo[];
20
+ aggregated?: Record<string, unknown>;
21
+ onupdate: (block: ProseBlock) => void;
22
+ onremove: () => void;
23
+ onclose: () => void;
24
+ }
25
+
26
+ let { block, runes, aggregated = {}, onupdate, onremove, onclose }: Props = $props();
27
+
28
+ type TabId = 'structure' | 'content';
29
+ let activeTab: TabId = $state('structure');
30
+
31
+ // ── Structure tab: child list ────────────────────────────────
32
+
33
+ /** Short preview text for a child block */
34
+ function childPreview(child: ParsedBlock): string {
35
+ switch (child.type) {
36
+ case 'heading':
37
+ return (child as HeadingBlock).text;
38
+ case 'paragraph':
39
+ return child.source.length > 60 ? child.source.slice(0, 60) + '...' : child.source;
40
+ case 'fence': {
41
+ const fb = child as FenceBlock;
42
+ const lang = fb.language ? `${fb.language}: ` : '';
43
+ const code = fb.code.split('\n')[0] ?? '';
44
+ return lang + (code.length > 50 ? code.slice(0, 50) + '...' : code);
45
+ }
46
+ case 'list':
47
+ return child.source.split('\n')[0] ?? '';
48
+ case 'quote':
49
+ return child.source.replace(/^>\s*/gm, '').split('\n')[0] ?? '';
50
+ case 'hr':
51
+ return '---';
52
+ case 'image':
53
+ return child.source;
54
+ default:
55
+ return child.source.split('\n')[0] ?? '';
56
+ }
57
+ }
58
+
59
+ /** Icon SVG for a child block type */
60
+ function childIcon(child: ParsedBlock): string {
61
+ switch (child.type) {
62
+ case 'heading':
63
+ return '<path d="M3 3v10M13 3v10M3 8h10" />';
64
+ case 'paragraph':
65
+ return '<path d="M2 4h12M2 8h12M2 12h8" />';
66
+ case 'fence':
67
+ return '<polyline points="4 6 1 9 4 12" /><polyline points="12 6 15 9 12 12" />';
68
+ case 'list':
69
+ return '<circle cx="3" cy="4" r="1" fill="currentColor" /><line x1="6" y1="4" x2="14" y2="4" /><circle cx="3" cy="8" r="1" fill="currentColor" /><line x1="6" y1="8" x2="14" y2="8" /><circle cx="3" cy="12" r="1" fill="currentColor" /><line x1="6" y1="12" x2="14" y2="12" />';
70
+ case 'quote':
71
+ return '<path d="M3 5h10M5 5v8M5 9h6" />';
72
+ case 'hr':
73
+ return '<line x1="2" y1="8" x2="14" y2="8" />';
74
+ case 'image':
75
+ return '<rect x="2" y="2" width="12" height="12" rx="1" /><circle cx="5.5" cy="5.5" r="1.5" /><path d="M14 10l-3-3-5 5" />';
76
+ default:
77
+ return '<path d="M2 4h12M2 8h12M2 12h8" />';
78
+ }
79
+ }
80
+
81
+ // ── Drag reorder ─────────────────────────────────────────────
82
+
83
+ let dragIdx: number | null = $state(null);
84
+ let dropIdx: number | null = $state(null);
85
+
86
+ function handleDragStart(e: DragEvent, idx: number) {
87
+ dragIdx = idx;
88
+ if (e.dataTransfer) {
89
+ e.dataTransfer.effectAllowed = 'move';
90
+ e.dataTransfer.setData('text/plain', String(idx));
91
+ }
92
+ }
93
+
94
+ function handleDragOver(e: DragEvent, idx: number) {
95
+ e.preventDefault();
96
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
97
+ dropIdx = idx;
98
+ }
99
+
100
+ function handleDrop(e: DragEvent, idx: number) {
101
+ e.preventDefault();
102
+ if (dragIdx !== null && dragIdx !== idx) {
103
+ reorderChild(dragIdx, idx);
104
+ }
105
+ dragIdx = null;
106
+ dropIdx = null;
107
+ }
108
+
109
+ function handleDragEnd() {
110
+ dragIdx = null;
111
+ dropIdx = null;
112
+ }
113
+
114
+ function reorderChild(from: number, to: number) {
115
+ const children = [...block.children];
116
+ const [moved] = children.splice(from, 1);
117
+ children.splice(to, 0, moved);
118
+ const newSource = serializeBlocks(children);
119
+ onupdate({ ...block, children, source: newSource });
120
+ }
121
+
122
+ function removeChild(idx: number) {
123
+ const children = block.children.filter((_, i) => i !== idx);
124
+ if (children.length === 0) {
125
+ onremove();
126
+ return;
127
+ }
128
+ const newSource = serializeBlocks(children);
129
+ onupdate({ ...block, children, source: newSource });
130
+ }
131
+
132
+ // ── Content tab ──────────────────────────────────────────────
133
+
134
+ function handleContentChange(content: string) {
135
+ if (!content.trim()) {
136
+ onremove();
137
+ return;
138
+ }
139
+ const newChildren = parseBlocks(content);
140
+ onupdate({ ...block, children: newChildren, source: content });
141
+ }
142
+ </script>
143
+
144
+ <div class="edit-panel">
145
+ <div class="edit-panel__top">
146
+ <div class="edit-panel__header">
147
+ <span class="edit-panel__type">prose</span>
148
+ <span class="edit-panel__count">{block.children.length} block{block.children.length !== 1 ? 's' : ''}</span>
149
+ <div class="edit-panel__spacer"></div>
150
+ <button
151
+ class="edit-panel__btn edit-panel__btn--danger"
152
+ onclick={onremove}
153
+ title="Remove block"
154
+ >
155
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
156
+ <polyline points="3 6 3 14 13 14 13 6" />
157
+ <line x1="1" y1="4" x2="15" y2="4" />
158
+ <line x1="6" y1="2" x2="10" y2="2" />
159
+ <line x1="6" y1="8" x2="6" y2="12" />
160
+ <line x1="10" y1="8" x2="10" y2="12" />
161
+ </svg>
162
+ </button>
163
+ <button
164
+ class="edit-panel__btn"
165
+ onclick={onclose}
166
+ title="Close panel"
167
+ >&times;</button>
168
+ </div>
169
+
170
+ <div class="edit-panel__tabs">
171
+ <button
172
+ type="button"
173
+ class="edit-panel__tab"
174
+ class:active={activeTab === 'structure'}
175
+ onclick={() => activeTab = 'structure'}
176
+ >
177
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
178
+ <path d="M2 3h4M2 7h4M6 11h4M6 15h4M4 3v8M8 11v4" />
179
+ </svg>
180
+ Structure
181
+ </button>
182
+ <button
183
+ type="button"
184
+ class="edit-panel__tab"
185
+ class:active={activeTab === 'content'}
186
+ onclick={() => activeTab = 'content'}
187
+ >
188
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
189
+ <path d="M2 4h12M2 8h12M2 12h8" />
190
+ </svg>
191
+ Content
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ {#if activeTab === 'structure'}
197
+ <div class="edit-panel__tab-panel">
198
+ <div class="structure-list">
199
+ {#each block.children as child, i (child.id)}
200
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
201
+ <div
202
+ class="structure-item"
203
+ class:drag-source={dragIdx === i}
204
+ class:drag-over={dropIdx === i && dragIdx !== i}
205
+ draggable="true"
206
+ ondragstart={(e) => handleDragStart(e, i)}
207
+ ondragover={(e) => handleDragOver(e, i)}
208
+ ondrop={(e) => handleDrop(e, i)}
209
+ ondragend={handleDragEnd}
210
+ >
211
+ <span class="structure-item__drag" title="Drag to reorder">
212
+ <svg width="8" height="12" viewBox="0 0 8 12" fill="currentColor">
213
+ <circle cx="2" cy="2" r="1" />
214
+ <circle cx="6" cy="2" r="1" />
215
+ <circle cx="2" cy="6" r="1" />
216
+ <circle cx="6" cy="6" r="1" />
217
+ <circle cx="2" cy="10" r="1" />
218
+ <circle cx="6" cy="10" r="1" />
219
+ </svg>
220
+ </span>
221
+ <span class="structure-item__icon">
222
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
223
+ {@html childIcon(child)}
224
+ </svg>
225
+ </span>
226
+ <span class="structure-item__info">
227
+ <span class="structure-item__type">{blockLabel(child)}</span>
228
+ <span class="structure-item__preview">{childPreview(child)}</span>
229
+ </span>
230
+ <button
231
+ class="structure-item__remove"
232
+ onclick={() => removeChild(i)}
233
+ title="Remove"
234
+ >
235
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
236
+ <line x1="4" y1="4" x2="12" y2="12" />
237
+ <line x1="12" y1="4" x2="4" y2="12" />
238
+ </svg>
239
+ </button>
240
+ </div>
241
+ {/each}
242
+ </div>
243
+ </div>
244
+ {/if}
245
+
246
+ {#if activeTab === 'content'}
247
+ <div class="edit-panel__tab-panel">
248
+ <div class="edit-panel__content-editor">
249
+ <InlineEditor
250
+ content={block.source}
251
+ onchange={handleContentChange}
252
+ {runes}
253
+ aggregated={() => aggregated}
254
+ />
255
+ </div>
256
+ </div>
257
+ {/if}
258
+ </div>
259
+
260
+ <style>
261
+ .edit-panel {
262
+ display: flex;
263
+ flex-direction: column;
264
+ flex: 1;
265
+ min-height: 0;
266
+ }
267
+
268
+ .edit-panel__top {
269
+ flex-shrink: 0;
270
+ background: var(--ed-surface-0);
271
+ border-bottom: 1px solid var(--ed-border-default);
272
+ }
273
+
274
+ .edit-panel__header {
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 0.5rem;
278
+ padding: var(--ed-space-4) var(--ed-space-5);
279
+ }
280
+
281
+ .edit-panel__type {
282
+ font-size: 12px;
283
+ font-weight: 700;
284
+ color: var(--ed-text-primary);
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.03em;
287
+ }
288
+
289
+ .edit-panel__count {
290
+ font-size: 11px;
291
+ color: var(--ed-text-muted);
292
+ }
293
+
294
+ .edit-panel__spacer {
295
+ flex: 1;
296
+ }
297
+
298
+ .edit-panel__btn {
299
+ background: none;
300
+ border: none;
301
+ color: var(--ed-text-muted);
302
+ cursor: pointer;
303
+ padding: 0.25rem;
304
+ font-size: 18px;
305
+ line-height: 1;
306
+ border-radius: var(--ed-radius-sm);
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ transition: color var(--ed-transition-fast), background var(--ed-transition-fast);
311
+ }
312
+
313
+ .edit-panel__btn:hover {
314
+ color: var(--ed-text-secondary);
315
+ background: var(--ed-surface-2);
316
+ }
317
+
318
+ .edit-panel__btn--danger:hover {
319
+ color: var(--ed-danger);
320
+ background: var(--ed-danger-subtle);
321
+ }
322
+
323
+ /* Tab strip */
324
+ .edit-panel__tabs {
325
+ display: flex;
326
+ gap: 2px;
327
+ background: var(--ed-surface-2);
328
+ border-radius: var(--ed-radius-sm);
329
+ padding: 2px;
330
+ margin: 0 var(--ed-space-4) var(--ed-space-3);
331
+ }
332
+
333
+ .edit-panel__tab {
334
+ flex: 1;
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ gap: 0.35rem;
339
+ padding: 0.35rem 0.5rem;
340
+ border: none;
341
+ background: transparent;
342
+ color: var(--ed-text-muted);
343
+ font-size: var(--ed-text-sm);
344
+ font-weight: 500;
345
+ cursor: pointer;
346
+ border-radius: calc(var(--ed-radius-sm) - 1px);
347
+ transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
348
+ }
349
+
350
+ .edit-panel__tab:hover {
351
+ color: var(--ed-text-secondary);
352
+ }
353
+
354
+ .edit-panel__tab.active {
355
+ background: var(--ed-surface-0);
356
+ color: var(--ed-text-primary);
357
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
358
+ }
359
+
360
+ /* Tab panels */
361
+ .edit-panel__tab-panel {
362
+ flex: 1;
363
+ overflow-y: auto;
364
+ display: flex;
365
+ flex-direction: column;
366
+ }
367
+
368
+ .edit-panel__content-editor {
369
+ display: flex;
370
+ flex-direction: column;
371
+ flex-shrink: 0;
372
+ overflow: hidden;
373
+ }
374
+
375
+ /* ── Structure list ───────────────────────────────────────── */
376
+
377
+ .structure-list {
378
+ display: flex;
379
+ flex-direction: column;
380
+ padding: var(--ed-space-3);
381
+ gap: 2px;
382
+ }
383
+
384
+ .structure-item {
385
+ display: flex;
386
+ align-items: center;
387
+ gap: var(--ed-space-2);
388
+ padding: var(--ed-space-2) var(--ed-space-3);
389
+ border-radius: var(--ed-radius-sm);
390
+ transition: background var(--ed-transition-fast), opacity var(--ed-transition-fast);
391
+ cursor: grab;
392
+ }
393
+
394
+ .structure-item:hover {
395
+ background: var(--ed-surface-2);
396
+ }
397
+
398
+ .structure-item.drag-source {
399
+ opacity: 0.4;
400
+ }
401
+
402
+ .structure-item.drag-over {
403
+ box-shadow: inset 0 -2px 0 var(--ed-accent);
404
+ }
405
+
406
+ .structure-item__drag {
407
+ color: var(--ed-text-muted);
408
+ opacity: 0.4;
409
+ flex-shrink: 0;
410
+ display: flex;
411
+ align-items: center;
412
+ }
413
+
414
+ .structure-item:hover .structure-item__drag {
415
+ opacity: 0.7;
416
+ }
417
+
418
+ .structure-item__icon {
419
+ color: var(--ed-text-tertiary);
420
+ flex-shrink: 0;
421
+ display: flex;
422
+ align-items: center;
423
+ }
424
+
425
+ .structure-item__info {
426
+ flex: 1;
427
+ min-width: 0;
428
+ display: flex;
429
+ flex-direction: column;
430
+ gap: 1px;
431
+ }
432
+
433
+ .structure-item__type {
434
+ font-size: 11px;
435
+ font-weight: 600;
436
+ color: var(--ed-text-secondary);
437
+ text-transform: uppercase;
438
+ letter-spacing: 0.02em;
439
+ }
440
+
441
+ .structure-item__preview {
442
+ font-size: 12px;
443
+ color: var(--ed-text-muted);
444
+ overflow: hidden;
445
+ text-overflow: ellipsis;
446
+ white-space: nowrap;
447
+ }
448
+
449
+ .structure-item__remove {
450
+ background: none;
451
+ border: none;
452
+ color: var(--ed-text-muted);
453
+ cursor: pointer;
454
+ padding: 0.2rem;
455
+ border-radius: var(--ed-radius-sm);
456
+ display: flex;
457
+ align-items: center;
458
+ opacity: 0;
459
+ transition: opacity var(--ed-transition-fast), color var(--ed-transition-fast), background var(--ed-transition-fast);
460
+ }
461
+
462
+ .structure-item:hover .structure-item__remove {
463
+ opacity: 1;
464
+ }
465
+
466
+ .structure-item__remove:hover {
467
+ color: var(--ed-danger);
468
+ background: var(--ed-danger-subtle);
469
+ }
470
+ </style>
@@ -103,6 +103,15 @@
103
103
  {#if info.required}
104
104
  <span class="rune-attrs__required">*</span>
105
105
  {/if}
106
+ {#if info.description}
107
+ <span class="rune-attrs__info" data-tooltip={info.description}>
108
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
109
+ <circle cx="8" cy="8" r="7" />
110
+ <line x1="8" y1="7" x2="8" y2="11" />
111
+ <circle cx="8" cy="4.5" r="0.5" fill="currentColor" stroke="none" />
112
+ </svg>
113
+ </span>
114
+ {/if}
106
115
  </span>
107
116
 
108
117
  {#if info.type === 'Boolean'}
@@ -220,6 +229,48 @@
220
229
  color: var(--ed-danger);
221
230
  }
222
231
 
232
+ .rune-attrs__info {
233
+ display: inline-flex;
234
+ align-items: center;
235
+ color: var(--ed-text-muted);
236
+ cursor: help;
237
+ position: relative;
238
+ margin-left: 0.2rem;
239
+ vertical-align: middle;
240
+ transition: color var(--ed-transition-fast);
241
+ }
242
+
243
+ .rune-attrs__info:hover {
244
+ color: var(--ed-accent);
245
+ }
246
+
247
+ .rune-attrs__info::after {
248
+ content: attr(data-tooltip);
249
+ position: absolute;
250
+ left: 0;
251
+ top: calc(100% + 6px);
252
+ background: var(--ed-surface-invert, #1a1a1a);
253
+ color: var(--ed-text-invert, #f0f0f0);
254
+ font-size: var(--ed-text-sm);
255
+ font-style: normal;
256
+ font-weight: 400;
257
+ padding: 0.35rem 0.5rem;
258
+ border-radius: var(--ed-radius-sm);
259
+ box-shadow: var(--ed-shadow-lg);
260
+ white-space: normal;
261
+ width: max-content;
262
+ max-width: 220px;
263
+ line-height: 1.4;
264
+ pointer-events: none;
265
+ opacity: 0;
266
+ transition: opacity var(--ed-transition-fast);
267
+ z-index: 20;
268
+ }
269
+
270
+ .rune-attrs__info:hover::after {
271
+ opacity: 1;
272
+ }
273
+
223
274
  /* Clickable value button */
224
275
  .rune-attrs__value {
225
276
  font-size: var(--ed-text-base);