@refrakt-md/editor 0.8.2 → 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.
- package/app/dist/assets/{index-Bn8ajfVl.js → index-3MvwKRVQ.js} +1 -1
- package/app/dist/assets/{index-DNtuldOx.js → index-B7e694w6.js} +1 -1
- package/app/dist/assets/{index-xo7v6nRB.js → index-BBljOYQu.js} +1 -1
- package/app/dist/assets/{index-DQUOY-pF.js → index-BEGy_i8o.js} +1 -1
- package/app/dist/assets/{index-DNJBunzP.js → index-BGy7ixjW.js} +1 -1
- package/app/dist/assets/{index-dGztG-54.js → index-BaLgiiKk.js} +1 -1
- package/app/dist/assets/{index-D5ucdUTo.js → index-BjlNcvOf.js} +1 -1
- package/app/dist/assets/{index-Cgbvx23V.js → index-CKfKYVw7.js} +1 -1
- package/app/dist/assets/{index-BDj1XPol.js → index-COFbngzR.js} +1 -1
- package/app/dist/assets/{index-aPeHMqUX.js → index-CPEo_rvd.js} +1 -1
- package/app/dist/assets/{index-BXe1fKaT.js → index-CQDCT-XT.js} +1 -1
- package/app/dist/assets/{index-CXeK-dZx.js → index-CUmEjEeR.js} +1 -1
- package/app/dist/assets/{index-80NtMar1.js → index-CeV-Af4N.js} +1 -1
- package/app/dist/assets/{index-CaRBCHaX.js → index-ChbH55h5.js} +1 -1
- package/app/dist/assets/index-CzvG5PZT.css +1 -0
- package/app/dist/assets/{index-BfxTGrHB.js → index-D9-aYc3I.js} +1 -1
- package/app/dist/assets/{index-DGYxLhpR.js → index-DezxtfNV.js} +1 -1
- package/app/dist/assets/{index-DskvyNKT.js → index-DrI4IfXE.js} +1 -1
- package/app/dist/assets/{index-CCkzIGTi.js → index-DwfxgjnU.js} +1 -1
- package/app/dist/assets/index-ogrpJNou.js +555 -0
- package/app/dist/index.html +2 -2
- package/app/src/lib/components/BlockEditor.svelte +381 -47
- package/app/src/lib/components/InlineEditor.svelte +15 -5
- package/app/src/lib/components/ProseBlockCard.svelte +446 -0
- package/app/src/lib/components/ProseEditPanel.svelte +470 -0
- package/app/src/lib/editor/block-parser.ts +59 -2
- package/package.json +6 -6
- package/app/dist/assets/index-B6H6LF1M.css +0 -1
- package/app/dist/assets/index-Cd12jZId.js +0 -479
|
@@ -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
|
+
>×</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>
|
|
@@ -68,6 +68,25 @@ export type ParsedBlock =
|
|
|
68
68
|
| ListBlock
|
|
69
69
|
| (Block & { type: 'paragraph' | 'quote' | 'hr' | 'image' });
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A prose block groups consecutive non-rune blocks into a single
|
|
73
|
+
* editing region. This is a view-layer concept — the underlying
|
|
74
|
+
* `ParsedBlock[]` array remains the source of truth.
|
|
75
|
+
*/
|
|
76
|
+
export interface ProseBlock {
|
|
77
|
+
id: string;
|
|
78
|
+
type: 'prose';
|
|
79
|
+
/** The child blocks that make up this prose group */
|
|
80
|
+
children: ParsedBlock[];
|
|
81
|
+
/** Combined source of all children, joined by blank lines */
|
|
82
|
+
source: string;
|
|
83
|
+
startLine: number;
|
|
84
|
+
endLine: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** An editor block is either a rune block (unchanged) or a prose block (grouped non-rune blocks) */
|
|
88
|
+
export type EditorBlock = RuneBlock | ProseBlock;
|
|
89
|
+
|
|
71
90
|
/** Deterministic hash (djb2) for stable block IDs across re-parses */
|
|
72
91
|
function hashSource(s: string): string {
|
|
73
92
|
let h = 5381;
|
|
@@ -1019,8 +1038,10 @@ export function extractRuneInner(example: string, name: string): string {
|
|
|
1019
1038
|
}
|
|
1020
1039
|
|
|
1021
1040
|
/** Human-readable label for a block, used in rail labels and edit panel header */
|
|
1022
|
-
export function blockLabel(block: ParsedBlock): string {
|
|
1041
|
+
export function blockLabel(block: ParsedBlock | EditorBlock): string {
|
|
1023
1042
|
switch (block.type) {
|
|
1043
|
+
case 'prose':
|
|
1044
|
+
return 'Prose';
|
|
1024
1045
|
case 'heading':
|
|
1025
1046
|
return `H${(block as HeadingBlock).level}`;
|
|
1026
1047
|
case 'rune':
|
|
@@ -1040,6 +1061,42 @@ export function blockLabel(block: ParsedBlock): string {
|
|
|
1040
1061
|
case 'paragraph':
|
|
1041
1062
|
return 'Paragraph';
|
|
1042
1063
|
default:
|
|
1043
|
-
return block.type;
|
|
1064
|
+
return (block as ParsedBlock).type;
|
|
1044
1065
|
}
|
|
1045
1066
|
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Group consecutive non-rune blocks into prose blocks.
|
|
1070
|
+
* Rune blocks pass through unchanged. Every non-rune block
|
|
1071
|
+
* (heading, paragraph, list, fence, quote, hr, image) becomes
|
|
1072
|
+
* part of the nearest prose group.
|
|
1073
|
+
*/
|
|
1074
|
+
export function groupIntoEditorBlocks(blocks: ParsedBlock[]): EditorBlock[] {
|
|
1075
|
+
const result: EditorBlock[] = [];
|
|
1076
|
+
let proseChildren: ParsedBlock[] = [];
|
|
1077
|
+
|
|
1078
|
+
function flushProse() {
|
|
1079
|
+
if (proseChildren.length === 0) return;
|
|
1080
|
+
const source = proseChildren.map(b => b.source).join('\n\n');
|
|
1081
|
+
result.push({
|
|
1082
|
+
id: `prose_${proseChildren[0].id}`,
|
|
1083
|
+
type: 'prose',
|
|
1084
|
+
children: [...proseChildren],
|
|
1085
|
+
source,
|
|
1086
|
+
startLine: proseChildren[0].startLine,
|
|
1087
|
+
endLine: proseChildren[proseChildren.length - 1].endLine,
|
|
1088
|
+
});
|
|
1089
|
+
proseChildren = [];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
for (const block of blocks) {
|
|
1093
|
+
if (block.type === 'rune') {
|
|
1094
|
+
flushProse();
|
|
1095
|
+
result.push(block);
|
|
1096
|
+
} else {
|
|
1097
|
+
proseChildren.push(block);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
flushProse();
|
|
1101
|
+
return result;
|
|
1102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refrakt-md/editor",
|
|
3
3
|
"description": "Browser-based content editor for refrakt.md",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@markdoc/markdoc": "0.4.0",
|
|
53
|
-
"@refrakt-md/content": "0.8.
|
|
54
|
-
"@refrakt-md/highlight": "0.8.
|
|
55
|
-
"@refrakt-md/runes": "0.8.
|
|
56
|
-
"@refrakt-md/transform": "0.8.
|
|
57
|
-
"@refrakt-md/types": "0.8.
|
|
53
|
+
"@refrakt-md/content": "0.8.3",
|
|
54
|
+
"@refrakt-md/highlight": "0.8.3",
|
|
55
|
+
"@refrakt-md/runes": "0.8.3",
|
|
56
|
+
"@refrakt-md/transform": "0.8.3",
|
|
57
|
+
"@refrakt-md/types": "0.8.3"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@codemirror/autocomplete": "^6.20.0",
|