@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,519 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { fetchAssets, uploadAsset, type ImageAsset } from '../api/client.js';
4
+
5
+ interface Props {
6
+ /** Current image path (e.g. "/hero.png") or empty for new */
7
+ currentSrc: string;
8
+ /** Current alt text */
9
+ currentAlt: string;
10
+ onchange: (src: string, alt: string) => void;
11
+ onremove: () => void;
12
+ onclose: () => void;
13
+ }
14
+
15
+ let { currentSrc, currentAlt, onchange, onremove, onclose }: Props = $props();
16
+
17
+ let modalEl: HTMLDivElement;
18
+ let fileInput: HTMLInputElement;
19
+ let altInput: HTMLInputElement;
20
+
21
+ let images = $state<ImageAsset[]>([]);
22
+ let loading = $state(true);
23
+ let uploading = $state(false);
24
+ let search = $state('');
25
+ let editSrc = $state(currentSrc);
26
+ let editAlt = $state(currentAlt);
27
+ let dragOver = $state(false);
28
+
29
+ const filtered = $derived(
30
+ search
31
+ ? images.filter(img => img.name.toLowerCase().includes(search.toLowerCase()))
32
+ : images
33
+ );
34
+
35
+ onMount(() => {
36
+ loadImages();
37
+
38
+ function handleKeydown(e: KeyboardEvent) {
39
+ if (e.key === 'Escape') onclose();
40
+ }
41
+
42
+ document.addEventListener('keydown', handleKeydown);
43
+ return () => document.removeEventListener('keydown', handleKeydown);
44
+ });
45
+
46
+ async function loadImages() {
47
+ loading = true;
48
+ try {
49
+ images = await fetchAssets();
50
+ } catch {
51
+ images = [];
52
+ }
53
+ loading = false;
54
+ }
55
+
56
+ function handleBackdropClick(e: MouseEvent) {
57
+ if (e.target === e.currentTarget) {
58
+ onclose();
59
+ }
60
+ }
61
+
62
+ function selectImage(img: ImageAsset) {
63
+ editSrc = img.path;
64
+ // If alt text is empty, suggest the filename without extension
65
+ if (!editAlt) {
66
+ const name = img.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
67
+ editAlt = name;
68
+ }
69
+ }
70
+
71
+ function handleApply() {
72
+ if (editSrc) {
73
+ onchange(editSrc, editAlt);
74
+ }
75
+ }
76
+
77
+ function handleUploadClick() {
78
+ fileInput?.click();
79
+ }
80
+
81
+ async function handleFileSelected(e: Event) {
82
+ const input = e.target as HTMLInputElement;
83
+ const file = input.files?.[0];
84
+ if (!file) return;
85
+ await doUpload(file);
86
+ input.value = '';
87
+ }
88
+
89
+ async function doUpload(file: File) {
90
+ if (!file.type.startsWith('image/')) return;
91
+ uploading = true;
92
+ try {
93
+ const result = await uploadAsset(file);
94
+ // Refresh list and select the new image
95
+ await loadImages();
96
+ editSrc = result.path;
97
+ if (!editAlt) {
98
+ const name = result.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
99
+ editAlt = name;
100
+ }
101
+ } catch (err) {
102
+ console.error('Upload failed:', err);
103
+ }
104
+ uploading = false;
105
+ }
106
+
107
+ function handleDragOver(e: DragEvent) {
108
+ e.preventDefault();
109
+ dragOver = true;
110
+ }
111
+
112
+ function handleDragLeave() {
113
+ dragOver = false;
114
+ }
115
+
116
+ async function handleDrop(e: DragEvent) {
117
+ e.preventDefault();
118
+ dragOver = false;
119
+ const file = e.dataTransfer?.files[0];
120
+ if (file) await doUpload(file);
121
+ }
122
+
123
+ function handleAltKeydown(e: KeyboardEvent) {
124
+ if (e.key === 'Enter') {
125
+ e.preventDefault();
126
+ handleApply();
127
+ }
128
+ }
129
+ </script>
130
+
131
+ <div
132
+ class="image-edit-backdrop"
133
+ onclick={handleBackdropClick}
134
+ onkeydown={() => {}}
135
+ role="dialog"
136
+ aria-modal="true"
137
+ aria-label="Select image"
138
+ >
139
+ <div
140
+ class="image-edit-modal"
141
+ class:drag-over={dragOver}
142
+ bind:this={modalEl}
143
+ ondragover={handleDragOver}
144
+ ondragleave={handleDragLeave}
145
+ ondrop={handleDrop}
146
+ >
147
+ <!-- Header -->
148
+ <div class="image-edit-modal__header">
149
+ <span class="image-edit-modal__label">image</span>
150
+ <button class="image-edit-modal__close" onclick={onclose} aria-label="Close">&times;</button>
151
+ </div>
152
+
153
+ <!-- Search -->
154
+ <div class="image-edit-modal__search">
155
+ <input
156
+ class="image-edit-modal__search-input"
157
+ type="text"
158
+ placeholder="Search images..."
159
+ bind:value={search}
160
+ />
161
+ </div>
162
+
163
+ <!-- Grid -->
164
+ <div class="image-edit-modal__grid-area">
165
+ {#if loading}
166
+ <div class="image-edit-modal__empty">Loading images...</div>
167
+ {:else if filtered.length === 0 && !search}
168
+ <div class="image-edit-modal__empty">No images found. Upload one to get started.</div>
169
+ {:else if filtered.length === 0}
170
+ <div class="image-edit-modal__empty">No images match "{search}"</div>
171
+ {:else}
172
+ <div class="image-edit-modal__grid">
173
+ {#each filtered as img (img.path)}
174
+ <button
175
+ class="image-edit-modal__thumb"
176
+ class:selected={editSrc === img.path}
177
+ onclick={() => selectImage(img)}
178
+ title={img.name}
179
+ >
180
+ <img src={img.path} alt={img.name} loading="lazy" />
181
+ </button>
182
+ {/each}
183
+ </div>
184
+ {/if}
185
+ </div>
186
+
187
+ <!-- Upload bar -->
188
+ <div class="image-edit-modal__upload-bar">
189
+ <button
190
+ class="image-edit-modal__upload-btn"
191
+ onclick={handleUploadClick}
192
+ disabled={uploading}
193
+ >
194
+ {uploading ? 'Uploading...' : 'Upload image'}
195
+ </button>
196
+ <span class="image-edit-modal__upload-hint">or drag & drop</span>
197
+ <input
198
+ type="file"
199
+ accept="image/*"
200
+ bind:this={fileInput}
201
+ onchange={handleFileSelected}
202
+ style="display: none"
203
+ />
204
+ </div>
205
+
206
+ <!-- Footer: alt text + actions -->
207
+ <div class="image-edit-modal__footer">
208
+ <div class="image-edit-modal__row">
209
+ <label class="image-edit-modal__field-label" for="image-alt">Alt</label>
210
+ <input
211
+ id="image-alt"
212
+ class="image-edit-modal__input"
213
+ type="text"
214
+ bind:value={editAlt}
215
+ bind:this={altInput}
216
+ onkeydown={handleAltKeydown}
217
+ placeholder="Describe this image..."
218
+ />
219
+ </div>
220
+ <div class="image-edit-modal__actions">
221
+ <button
222
+ class="image-edit-modal__btn image-edit-modal__btn--apply"
223
+ onclick={handleApply}
224
+ disabled={!editSrc}
225
+ >Apply</button>
226
+ <button
227
+ class="image-edit-modal__btn image-edit-modal__btn--remove"
228
+ onclick={onremove}
229
+ >Remove</button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+
235
+ <style>
236
+ /* ── Backdrop ─────────────────────────────────────────── */
237
+
238
+ .image-edit-backdrop {
239
+ position: fixed;
240
+ inset: 0;
241
+ z-index: 1100;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ background: rgba(0, 0, 0, 0.3);
246
+ animation: image-edit-backdrop-in 120ms ease-out;
247
+ }
248
+
249
+ @keyframes image-edit-backdrop-in {
250
+ from { opacity: 0; }
251
+ to { opacity: 1; }
252
+ }
253
+
254
+ /* ── Modal ────────────────────────────────────────────── */
255
+
256
+ .image-edit-modal {
257
+ z-index: 1101;
258
+ display: flex;
259
+ flex-direction: column;
260
+ width: 90vw;
261
+ max-width: 580px;
262
+ max-height: 80vh;
263
+ background: var(--ed-surface-0, #fff);
264
+ border: 1px solid var(--ed-border-default, #e2e8f0);
265
+ border-radius: var(--ed-radius-lg, 8px);
266
+ box-shadow: var(--ed-shadow-xl, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1));
267
+ animation: image-edit-enter 120ms ease-out;
268
+ overflow: hidden;
269
+ }
270
+
271
+ .image-edit-modal.drag-over {
272
+ outline: 2px dashed var(--ed-accent, #3b82f6);
273
+ outline-offset: -2px;
274
+ }
275
+
276
+ @keyframes image-edit-enter {
277
+ from { opacity: 0; transform: scale(0.97) translateY(8px); }
278
+ to { opacity: 1; transform: scale(1) translateY(0); }
279
+ }
280
+
281
+ /* ── Header ──────────────────────────────────────────── */
282
+
283
+ .image-edit-modal__header {
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: space-between;
287
+ padding: var(--ed-space-2, 0.5rem) var(--ed-space-3, 0.75rem);
288
+ border-bottom: 1px solid var(--ed-border-default, #e2e8f0);
289
+ }
290
+
291
+ .image-edit-modal__label {
292
+ font-size: 10px;
293
+ font-weight: 700;
294
+ color: var(--ed-text-muted, #94a3b8);
295
+ text-transform: uppercase;
296
+ letter-spacing: 0.04em;
297
+ }
298
+
299
+ .image-edit-modal__close {
300
+ background: none;
301
+ border: none;
302
+ font-size: 18px;
303
+ line-height: 1;
304
+ color: var(--ed-text-muted, #94a3b8);
305
+ cursor: pointer;
306
+ padding: 0 4px;
307
+ }
308
+
309
+ .image-edit-modal__close:hover {
310
+ color: var(--ed-text-primary, #1a1a2e);
311
+ }
312
+
313
+ /* ── Search ──────────────────────────────────────────── */
314
+
315
+ .image-edit-modal__search {
316
+ padding: var(--ed-space-2, 0.5rem) var(--ed-space-3, 0.75rem);
317
+ }
318
+
319
+ .image-edit-modal__search-input {
320
+ width: 100%;
321
+ padding: var(--ed-space-1, 0.25rem) var(--ed-space-2, 0.5rem);
322
+ border: 1px solid var(--ed-border-default, #e2e8f0);
323
+ border-radius: var(--ed-radius-sm, 4px);
324
+ font-size: var(--ed-text-sm, 13px);
325
+ color: var(--ed-text-primary, #1a1a2e);
326
+ background: var(--ed-surface-0, #fff);
327
+ outline: none;
328
+ font-family: inherit;
329
+ box-sizing: border-box;
330
+ }
331
+
332
+ .image-edit-modal__search-input:focus {
333
+ border-color: var(--ed-accent, #3b82f6);
334
+ box-shadow: 0 0 0 2px var(--ed-accent-ring, rgba(59, 130, 246, 0.2));
335
+ }
336
+
337
+ /* ── Grid area ───────────────────────────────────────── */
338
+
339
+ .image-edit-modal__grid-area {
340
+ flex: 1;
341
+ overflow-y: auto;
342
+ padding: 0 var(--ed-space-3, 0.75rem);
343
+ min-height: 120px;
344
+ max-height: 320px;
345
+ }
346
+
347
+ .image-edit-modal__grid {
348
+ display: grid;
349
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
350
+ gap: 8px;
351
+ padding-bottom: var(--ed-space-2, 0.5rem);
352
+ }
353
+
354
+ .image-edit-modal__empty {
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ height: 120px;
359
+ color: var(--ed-text-muted, #94a3b8);
360
+ font-size: var(--ed-text-sm, 13px);
361
+ }
362
+
363
+ /* ── Thumbnail ───────────────────────────────────────── */
364
+
365
+ .image-edit-modal__thumb {
366
+ position: relative;
367
+ aspect-ratio: 1;
368
+ border: 2px solid var(--ed-border-default, #e2e8f0);
369
+ border-radius: var(--ed-radius-sm, 4px);
370
+ overflow: hidden;
371
+ cursor: pointer;
372
+ padding: 0;
373
+ background: var(--ed-surface-1, #f8fafc);
374
+ transition: border-color 100ms;
375
+ }
376
+
377
+ .image-edit-modal__thumb:hover {
378
+ border-color: var(--ed-text-muted, #94a3b8);
379
+ }
380
+
381
+ .image-edit-modal__thumb.selected {
382
+ border-color: var(--ed-accent, #3b82f6);
383
+ box-shadow: 0 0 0 2px var(--ed-accent-ring, rgba(59, 130, 246, 0.3));
384
+ }
385
+
386
+ .image-edit-modal__thumb img {
387
+ width: 100%;
388
+ height: 100%;
389
+ object-fit: cover;
390
+ display: block;
391
+ }
392
+
393
+ /* ── Upload bar ──────────────────────────────────────── */
394
+
395
+ .image-edit-modal__upload-bar {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: var(--ed-space-2, 0.5rem);
399
+ padding: var(--ed-space-2, 0.5rem) var(--ed-space-3, 0.75rem);
400
+ border-top: 1px solid var(--ed-border-default, #e2e8f0);
401
+ }
402
+
403
+ .image-edit-modal__upload-btn {
404
+ padding: var(--ed-space-1, 0.25rem) var(--ed-space-3, 0.75rem);
405
+ border: 1px solid var(--ed-border-default, #e2e8f0);
406
+ border-radius: var(--ed-radius-sm, 4px);
407
+ font-size: 12px;
408
+ font-weight: 500;
409
+ cursor: pointer;
410
+ background: var(--ed-surface-0, #fff);
411
+ color: var(--ed-text-secondary, #475569);
412
+ transition: background 100ms;
413
+ }
414
+
415
+ .image-edit-modal__upload-btn:hover:not(:disabled) {
416
+ background: var(--ed-surface-2, #f1f5f9);
417
+ }
418
+
419
+ .image-edit-modal__upload-btn:disabled {
420
+ opacity: 0.6;
421
+ cursor: not-allowed;
422
+ }
423
+
424
+ .image-edit-modal__upload-hint {
425
+ font-size: 11px;
426
+ color: var(--ed-text-muted, #94a3b8);
427
+ }
428
+
429
+ /* ── Footer ──────────────────────────────────────────── */
430
+
431
+ .image-edit-modal__footer {
432
+ display: flex;
433
+ flex-direction: column;
434
+ gap: var(--ed-space-2, 0.5rem);
435
+ padding: var(--ed-space-2, 0.5rem) var(--ed-space-3, 0.75rem);
436
+ border-top: 1px solid var(--ed-border-default, #e2e8f0);
437
+ }
438
+
439
+ .image-edit-modal__row {
440
+ display: flex;
441
+ align-items: center;
442
+ gap: var(--ed-space-2, 0.5rem);
443
+ }
444
+
445
+ .image-edit-modal__field-label {
446
+ font-size: 10px;
447
+ font-weight: 600;
448
+ color: var(--ed-text-muted, #94a3b8);
449
+ text-transform: uppercase;
450
+ letter-spacing: 0.03em;
451
+ width: 24px;
452
+ flex-shrink: 0;
453
+ }
454
+
455
+ .image-edit-modal__input {
456
+ flex: 1;
457
+ min-width: 0;
458
+ padding: var(--ed-space-1, 0.25rem) var(--ed-space-2, 0.5rem);
459
+ border: 1px solid var(--ed-border-default, #e2e8f0);
460
+ border-radius: var(--ed-radius-sm, 4px);
461
+ font-size: var(--ed-text-sm, 13px);
462
+ color: var(--ed-text-primary, #1a1a2e);
463
+ background: var(--ed-surface-0, #fff);
464
+ outline: none;
465
+ font-family: inherit;
466
+ line-height: 1.4;
467
+ }
468
+
469
+ .image-edit-modal__input:focus {
470
+ border-color: var(--ed-accent, #3b82f6);
471
+ box-shadow: 0 0 0 2px var(--ed-accent-ring, rgba(59, 130, 246, 0.2));
472
+ }
473
+
474
+ /* ── Actions ─────────────────────────────────────────── */
475
+
476
+ .image-edit-modal__actions {
477
+ display: flex;
478
+ gap: var(--ed-space-2, 0.5rem);
479
+ }
480
+
481
+ .image-edit-modal__btn {
482
+ display: flex;
483
+ align-items: center;
484
+ justify-content: center;
485
+ gap: var(--ed-space-2);
486
+ height: 28px;
487
+ padding: 0 var(--ed-space-3);
488
+ border: 1px solid var(--ed-text-secondary);
489
+ border-radius: var(--ed-radius-sm);
490
+ background: transparent;
491
+ color: var(--ed-text-secondary);
492
+ font-size: var(--ed-text-sm);
493
+ font-weight: 500;
494
+ cursor: pointer;
495
+ transition: background var(--ed-transition-fast), color var(--ed-transition-fast), border-color var(--ed-transition-fast);
496
+ white-space: nowrap;
497
+ }
498
+
499
+ .image-edit-modal__btn:hover:not(.image-edit-modal__btn--apply):not(:disabled) {
500
+ border-color: var(--ed-text-primary);
501
+ color: var(--ed-text-primary);
502
+ }
503
+
504
+ .image-edit-modal__btn--apply {
505
+ background: var(--ed-text-primary);
506
+ color: #ffffff;
507
+ border-color: var(--ed-text-primary);
508
+ }
509
+
510
+ .image-edit-modal__btn--apply:hover:not(:disabled) {
511
+ background: var(--ed-text-secondary);
512
+ border-color: var(--ed-text-secondary);
513
+ }
514
+
515
+ .image-edit-modal__btn--apply:disabled {
516
+ opacity: 0.4;
517
+ cursor: not-allowed;
518
+ }
519
+ </style>