@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.
- package/app/dist/assets/{index-Ca-wW6uw.js → index-80NtMar1.js} +1 -1
- package/app/dist/assets/index-B6H6LF1M.css +1 -0
- package/app/dist/assets/{index-Dg4A5Pez.js → index-BDj1XPol.js} +1 -1
- package/app/dist/assets/{index-BfYWp0QC.js → index-BXe1fKaT.js} +1 -1
- package/app/dist/assets/{index-Cq0Maciq.js → index-BfxTGrHB.js} +1 -1
- package/app/dist/assets/{index-BsSUa0GD.js → index-Bn8ajfVl.js} +1 -1
- package/app/dist/assets/{index-D6vnTt4b.js → index-CCkzIGTi.js} +2 -2
- package/app/dist/assets/{index-BehCztSl.js → index-CXeK-dZx.js} +1 -1
- package/app/dist/assets/{index-iGDqoXj_.js → index-CaRBCHaX.js} +1 -1
- package/app/dist/assets/index-Cd12jZId.js +479 -0
- package/app/dist/assets/{index-D5pMhPrg.js → index-Cgbvx23V.js} +1 -1
- package/app/dist/assets/{index-IU6QYZAa.js → index-D5ucdUTo.js} +1 -1
- package/app/dist/assets/{index-CdpS6tGk.js → index-DGYxLhpR.js} +1 -1
- package/app/dist/assets/{index-RKEq45V5.js → index-DNJBunzP.js} +1 -1
- package/app/dist/assets/{index-Cgaw2jCE.js → index-DNtuldOx.js} +1 -1
- package/app/dist/assets/{index-BEPqnnsd.js → index-DQUOY-pF.js} +1 -1
- package/app/dist/assets/{index-2hOoPFOR.js → index-DskvyNKT.js} +1 -1
- package/app/dist/assets/{index-CLZfwYyS.js → index-aPeHMqUX.js} +1 -1
- package/app/dist/assets/{index-BobjskUl.js → index-dGztG-54.js} +1 -1
- package/app/dist/assets/{index-DHALjxX5.js → index-xo7v6nRB.js} +1 -1
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +81 -0
- package/app/src/lib/components/ActionEditPopover.svelte +267 -0
- package/app/src/lib/components/BlockCard.svelte +285 -0
- package/app/src/lib/components/BlockEditPanel.svelte +640 -260
- package/app/src/lib/components/BlockEditor.svelte +513 -52
- package/app/src/lib/components/CodeEditPopover.svelte +444 -0
- package/app/src/lib/components/ContentModelTree.svelte +835 -0
- package/app/src/lib/components/EditorLayout.svelte +1 -6
- package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
- package/app/src/lib/components/IconPickerPopover.svelte +389 -0
- package/app/src/lib/components/ImageEditPopover.svelte +519 -0
- package/app/src/lib/components/InlineEditPopover.svelte +616 -0
- package/app/src/lib/components/RuneAttributes.svelte +51 -0
- package/app/src/lib/editor/block-parser.ts +424 -6
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +189 -2
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/app/dist/assets/index-98ylvoBO.css +0 -1
- package/app/dist/assets/index-CVzOx0nV.js +0 -372
|
@@ -15,9 +15,22 @@
|
|
|
15
15
|
parseContentTree,
|
|
16
16
|
serializeAttributes,
|
|
17
17
|
replaceNodeSource,
|
|
18
|
+
insertFieldContent,
|
|
19
|
+
removeFieldContent,
|
|
20
|
+
appendListItem,
|
|
21
|
+
removeListItem,
|
|
22
|
+
reorderListItem,
|
|
23
|
+
appendGreedyItem,
|
|
24
|
+
removeGreedyItem,
|
|
25
|
+
reorderGreedyItem,
|
|
26
|
+
splitListItems,
|
|
18
27
|
} from '../editor/block-parser.js';
|
|
28
|
+
import { resolveContentStructure, type ResolvedField } from '../editor/content-model-resolver.js';
|
|
29
|
+
import type { SectionMapping, CommandMapping } from '../editor/section-mapper.js';
|
|
30
|
+
import { stripInlineMarkdown } from '../editor/inline-markdown.js';
|
|
19
31
|
import RuneAttributes from './RuneAttributes.svelte';
|
|
20
32
|
import ContentTree from './ContentTree.svelte';
|
|
33
|
+
import ContentModelTree from './ContentModelTree.svelte';
|
|
21
34
|
import InlineEditor from './InlineEditor.svelte';
|
|
22
35
|
|
|
23
36
|
interface Props {
|
|
@@ -25,12 +38,16 @@
|
|
|
25
38
|
runeMap: Map<string, RuneInfo>;
|
|
26
39
|
runes: () => RuneInfo[];
|
|
27
40
|
aggregated?: Record<string, unknown>;
|
|
41
|
+
/** When set, auto-navigate to the Nth nested rune (DFS order) on mount */
|
|
42
|
+
initialRuneIndex?: number | null;
|
|
28
43
|
onupdate: (block: ParsedBlock) => void;
|
|
29
44
|
onremove: () => void;
|
|
30
45
|
onclose: () => void;
|
|
46
|
+
oneditfield?: (dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) => void;
|
|
47
|
+
oneditcode?: (code: string, language: string, rect: DOMRect, mapping: CommandMapping) => void;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
let { block, runeMap, runes, aggregated = {}, onupdate, onremove, onclose }: Props = $props();
|
|
50
|
+
let { block, runeMap, runes, aggregated = {}, initialRuneIndex = null, onupdate, onremove, onclose, oneditfield, oneditcode }: Props = $props();
|
|
34
51
|
|
|
35
52
|
let label = $derived(blockLabel(block));
|
|
36
53
|
|
|
@@ -58,6 +75,55 @@
|
|
|
58
75
|
contentTree.some(n => n.type === 'rune')
|
|
59
76
|
);
|
|
60
77
|
|
|
78
|
+
/** The effective rune info for the structure tab — nested rune if selected, root otherwise */
|
|
79
|
+
let effectiveRuneInfo = $derived.by(() => {
|
|
80
|
+
if (activeNode?.type === 'rune' && activeRuneInfo) return activeRuneInfo;
|
|
81
|
+
return runeInfo;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Content tree for the effective rune (nested rune's children or root's) */
|
|
85
|
+
let effectiveContentTree = $derived.by(() => {
|
|
86
|
+
if (activeNode?.type === 'rune' && activeNode.children) return activeNode.children;
|
|
87
|
+
return contentTree;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** Whether the effective rune has a declarative content model */
|
|
91
|
+
let hasContentModel = $derived(effectiveRuneInfo?.contentModel != null);
|
|
92
|
+
|
|
93
|
+
/** Resolved structure: effective content tree matched against effective content model */
|
|
94
|
+
let resolvedStructure = $derived.by(() => {
|
|
95
|
+
if (!effectiveRuneInfo?.contentModel) return null;
|
|
96
|
+
return resolveContentStructure(effectiveContentTree, effectiveRuneInfo.contentModel);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** Currently selected field in the content model tree */
|
|
100
|
+
let selectedField: string | null = $state(null);
|
|
101
|
+
|
|
102
|
+
/** Find the path to the Nth rune node in DFS order */
|
|
103
|
+
function findRunePathByDfsIndex(nodes: ContentNode[], target: number): number[] | null {
|
|
104
|
+
let count = 0;
|
|
105
|
+
function walk(ns: ContentNode[], path: number[]): number[] | null {
|
|
106
|
+
for (let i = 0; i < ns.length; i++) {
|
|
107
|
+
if (ns[i].type === 'rune') {
|
|
108
|
+
if (count === target) return [...path, i];
|
|
109
|
+
count++;
|
|
110
|
+
const found = walk(ns[i].children ?? [], [...path, i]);
|
|
111
|
+
if (found) return found;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return walk(nodes, []);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Auto-navigate to a nested rune when initialRuneIndex is provided
|
|
120
|
+
$effect(() => {
|
|
121
|
+
if (initialRuneIndex != null && contentTree.length > 0) {
|
|
122
|
+
const path = findRunePathByDfsIndex(contentTree, initialRuneIndex);
|
|
123
|
+
if (path) activePath = path;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
61
127
|
/** Resolve the active nested node from the path */
|
|
62
128
|
function resolveNode(nodes: ContentNode[], path: number[]): ContentNode | null {
|
|
63
129
|
if (path.length === 0) return null;
|
|
@@ -84,36 +150,253 @@
|
|
|
84
150
|
return activeNode.type;
|
|
85
151
|
});
|
|
86
152
|
|
|
87
|
-
|
|
88
|
-
let
|
|
153
|
+
type TabId = 'settings' | 'structure' | 'content';
|
|
154
|
+
let activeTab: TabId = $state('settings');
|
|
155
|
+
|
|
156
|
+
let availableTabs = $derived.by(() => {
|
|
157
|
+
if (block.type !== 'rune') return [] as TabId[];
|
|
158
|
+
const rb = block as RuneBlock;
|
|
159
|
+
const tabs: TabId[] = ['settings'];
|
|
160
|
+
// Show structure tab when the effective rune has a content model,
|
|
161
|
+
// or when no nested rune is selected and root has nested runes (legacy tree)
|
|
162
|
+
if (hasContentModel || (!activeNode && hasNestedRunes)) tabs.push('structure');
|
|
163
|
+
if (!rb.selfClosing) tabs.push('content');
|
|
164
|
+
return tabs;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Auto-switch away from structure tab if it becomes unavailable
|
|
168
|
+
$effect(() => {
|
|
169
|
+
if (activeTab === 'structure' && !availableTabs.includes('structure')) {
|
|
170
|
+
activeTab = 'settings';
|
|
171
|
+
}
|
|
172
|
+
});
|
|
89
173
|
|
|
90
174
|
function handleTreeSelect(path: number[]) {
|
|
91
175
|
activePath = path;
|
|
92
|
-
|
|
176
|
+
// Auto-switch to Content tab when selecting a content node
|
|
177
|
+
const node = resolveNode(contentTree, path);
|
|
178
|
+
if (node && node.type !== 'rune') {
|
|
179
|
+
activeTab = 'content';
|
|
180
|
+
}
|
|
93
181
|
}
|
|
94
182
|
|
|
95
183
|
function navigateToRoot() {
|
|
96
184
|
activePath = [];
|
|
97
|
-
showTreeDropdown = false;
|
|
98
185
|
}
|
|
99
186
|
|
|
100
|
-
|
|
101
|
-
|
|
187
|
+
// ── Content model field handlers ─────────────────────────────
|
|
188
|
+
|
|
189
|
+
function handleFieldSelect(fieldName: string, zoneName?: string) {
|
|
190
|
+
selectedField = zoneName ? `${zoneName}.${fieldName}` : fieldName;
|
|
102
191
|
}
|
|
103
192
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
193
|
+
/** Apply a field content change — works for both root and nested runes */
|
|
194
|
+
function applyFieldChange(updater: (content: string) => string) {
|
|
195
|
+
const rb = block as RuneBlock;
|
|
196
|
+
if (activeNode?.type === 'rune' && activeNode.innerContent !== undefined) {
|
|
197
|
+
// Nested rune: update its inner content, then replace in root
|
|
198
|
+
const newNestedInner = updater(activeNode.innerContent);
|
|
199
|
+
if (newNestedInner === activeNode.innerContent) return;
|
|
200
|
+
const attrStr = serializeAttributes(activeNode.attributes ?? {});
|
|
201
|
+
const inner = newNestedInner.trim();
|
|
202
|
+
const newSource = inner
|
|
203
|
+
? `{% ${activeNode.runeName}${attrStr} %}\n${inner}\n{% /${activeNode.runeName} %}`
|
|
204
|
+
: `{% ${activeNode.runeName}${attrStr} %}\n\n{% /${activeNode.runeName} %}`;
|
|
205
|
+
const newRootInner = replaceNodeSource(rb.innerContent, activeNode.source, newSource);
|
|
206
|
+
const updated: RuneBlock = { ...rb, innerContent: newRootInner, source: '' };
|
|
207
|
+
updated.source = rebuildRuneSource(updated);
|
|
208
|
+
onupdate(updated);
|
|
209
|
+
} else {
|
|
210
|
+
// Root rune
|
|
211
|
+
const newInner = updater(rb.innerContent);
|
|
212
|
+
if (newInner === rb.innerContent) return;
|
|
213
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
214
|
+
updated.source = rebuildRuneSource(updated);
|
|
215
|
+
onupdate(updated);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleAddField(fieldName: string, zoneName?: string) {
|
|
220
|
+
if (!resolvedStructure) return;
|
|
221
|
+
applyFieldChange(content => insertFieldContent(content, resolvedStructure!, fieldName, zoneName));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleRemoveField(fieldName: string, zoneName?: string) {
|
|
225
|
+
if (!resolvedStructure) return;
|
|
226
|
+
applyFieldChange(content => removeFieldContent(content, resolvedStructure!, fieldName, zoneName));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function findResolvedField(fieldName: string, zoneName?: string): ResolvedField | null {
|
|
230
|
+
if (!resolvedStructure) return null;
|
|
231
|
+
if (resolvedStructure.type === 'sequence') {
|
|
232
|
+
return resolvedStructure.fields.find(f => f.name === fieldName) ?? null;
|
|
233
|
+
}
|
|
234
|
+
if (resolvedStructure.type === 'delimited' && zoneName) {
|
|
235
|
+
const zone = resolvedStructure.zones.find(z => z.name === zoneName);
|
|
236
|
+
return zone?.fields.find(f => f.name === fieldName) ?? null;
|
|
107
237
|
}
|
|
238
|
+
return null;
|
|
108
239
|
}
|
|
109
240
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
241
|
+
function isGreedyItemField(field: ResolvedField): boolean {
|
|
242
|
+
return field.greedy && field.match !== 'any';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleAppendItem(fieldName: string, zoneName?: string) {
|
|
246
|
+
if (!resolvedStructure) return;
|
|
247
|
+
const field = findResolvedField(fieldName, zoneName);
|
|
248
|
+
if (field && isGreedyItemField(field)) {
|
|
249
|
+
applyFieldChange(content => appendGreedyItem(content, resolvedStructure!, fieldName, zoneName));
|
|
250
|
+
} else {
|
|
251
|
+
applyFieldChange(content => appendListItem(content, resolvedStructure!, fieldName, zoneName));
|
|
114
252
|
}
|
|
115
253
|
}
|
|
116
254
|
|
|
255
|
+
function handleRemoveListItem(fieldName: string, itemIndex: number, zoneName?: string) {
|
|
256
|
+
if (!resolvedStructure) return;
|
|
257
|
+
const field = findResolvedField(fieldName, zoneName);
|
|
258
|
+
if (field && isGreedyItemField(field)) {
|
|
259
|
+
applyFieldChange(content => removeGreedyItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
|
|
260
|
+
} else {
|
|
261
|
+
applyFieldChange(content => removeListItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function handleReorderListItem(fieldName: string, fromIndex: number, toIndex: number, zoneName?: string) {
|
|
266
|
+
if (!resolvedStructure) return;
|
|
267
|
+
const field = findResolvedField(fieldName, zoneName);
|
|
268
|
+
if (field && isGreedyItemField(field)) {
|
|
269
|
+
applyFieldChange(content => reorderGreedyItem(content, resolvedStructure!, fieldName, fromIndex, toIndex, zoneName));
|
|
270
|
+
} else {
|
|
271
|
+
applyFieldChange(content => reorderListItem(content, resolvedStructure!, fieldName, fromIndex, toIndex, zoneName));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function handleEditField(fieldName: string, rect: DOMRect, zoneName?: string, nodeIndex?: number) {
|
|
276
|
+
if (!resolvedStructure) return;
|
|
277
|
+
// Find the field in the resolved structure
|
|
278
|
+
let field;
|
|
279
|
+
if (resolvedStructure.type === 'sequence') {
|
|
280
|
+
field = resolvedStructure.fields.find(f => f.name === fieldName);
|
|
281
|
+
} else if (resolvedStructure.type === 'delimited' && zoneName) {
|
|
282
|
+
const zone = resolvedStructure.zones.find(z => z.name === zoneName);
|
|
283
|
+
field = zone?.fields.find(f => f.name === fieldName);
|
|
284
|
+
}
|
|
285
|
+
if (!field || !field.filled) return;
|
|
286
|
+
|
|
287
|
+
// Pick the target node — use nodeIndex for greedy fields, default to single-node fields
|
|
288
|
+
const targetIndex = nodeIndex ?? 0;
|
|
289
|
+
if (targetIndex >= field.nodes.length) return;
|
|
290
|
+
if (nodeIndex === undefined && field.nodes.length !== 1) return;
|
|
291
|
+
|
|
292
|
+
const targetNode = field.nodes[targetIndex];
|
|
293
|
+
const source = targetNode.source;
|
|
294
|
+
|
|
295
|
+
// Fence nodes → open code editor instead of inline text editor
|
|
296
|
+
if (targetNode.type === 'fence' && oneditcode) {
|
|
297
|
+
const code = targetNode.fenceCode ?? '';
|
|
298
|
+
const language = targetNode.fenceLanguage ?? '';
|
|
299
|
+
const opener = source.split('\n')[0];
|
|
300
|
+
const delimMatch = opener.match(/^(`{3,}|~{3,})/);
|
|
301
|
+
const delimiter = delimMatch ? delimMatch[1] : '```';
|
|
302
|
+
const mapping: CommandMapping = { source, code, language, opener, delimiter };
|
|
303
|
+
oneditcode(code, language, rect, mapping);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!oneditfield) return;
|
|
308
|
+
|
|
309
|
+
const trimmed = source.trim();
|
|
310
|
+
|
|
311
|
+
// Strip markdown prefix (heading markers, blockquote markers)
|
|
312
|
+
let prefix = '';
|
|
313
|
+
let inlineContent = trimmed;
|
|
314
|
+
const headingMatch = trimmed.match(/^(#{1,6}\s+)(.*)/);
|
|
315
|
+
if (headingMatch) {
|
|
316
|
+
prefix = headingMatch[1];
|
|
317
|
+
inlineContent = headingMatch[2];
|
|
318
|
+
} else {
|
|
319
|
+
const quoteMatch = trimmed.match(/^(>\s*)(.*)/);
|
|
320
|
+
if (quoteMatch) {
|
|
321
|
+
prefix = quoteMatch[1];
|
|
322
|
+
inlineContent = quoteMatch[2];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const mapping: SectionMapping = {
|
|
327
|
+
dataName: fieldName,
|
|
328
|
+
text: stripInlineMarkdown(inlineContent),
|
|
329
|
+
source,
|
|
330
|
+
sourcePrefix: prefix,
|
|
331
|
+
inlineSource: inlineContent,
|
|
332
|
+
};
|
|
333
|
+
oneditfield(fieldName, inlineContent, rect, mapping);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function handleEditListItem(fieldName: string, itemIndex: number, rect: DOMRect, zoneName?: string) {
|
|
337
|
+
if (!resolvedStructure || !oneditfield) return;
|
|
338
|
+
// Find the field in the resolved structure
|
|
339
|
+
let field;
|
|
340
|
+
if (resolvedStructure.type === 'sequence') {
|
|
341
|
+
field = resolvedStructure.fields.find(f => f.name === fieldName);
|
|
342
|
+
} else if (resolvedStructure.type === 'delimited' && zoneName) {
|
|
343
|
+
const zone = resolvedStructure.zones.find(z => z.name === zoneName);
|
|
344
|
+
field = zone?.fields.find(f => f.name === fieldName);
|
|
345
|
+
}
|
|
346
|
+
if (!field || !field.filled || field.nodes.length === 0) return;
|
|
347
|
+
|
|
348
|
+
const listNode = field.nodes[0];
|
|
349
|
+
const items = splitListItems(listNode.source);
|
|
350
|
+
if (itemIndex < 0 || itemIndex >= items.length) return;
|
|
351
|
+
|
|
352
|
+
const itemSource = items[itemIndex];
|
|
353
|
+
// Strip the list marker to get inline content
|
|
354
|
+
const markerMatch = itemSource.match(/^([-*+]\s+|\d+\.\s+)/);
|
|
355
|
+
const prefix = markerMatch ? markerMatch[1] : '';
|
|
356
|
+
const inlineContent = markerMatch ? itemSource.slice(prefix.length) : itemSource;
|
|
357
|
+
|
|
358
|
+
const mapping: SectionMapping = {
|
|
359
|
+
dataName: `${fieldName}[${itemIndex}]`,
|
|
360
|
+
text: stripInlineMarkdown(inlineContent),
|
|
361
|
+
source: itemSource,
|
|
362
|
+
sourcePrefix: prefix,
|
|
363
|
+
inlineSource: inlineContent,
|
|
364
|
+
};
|
|
365
|
+
oneditfield(`${fieldName}[${itemIndex}]`, inlineContent, rect, mapping);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function handleNavigateRune(fieldName: string, nodeIndex: number, zoneName?: string) {
|
|
369
|
+
if (!resolvedStructure) return;
|
|
370
|
+
|
|
371
|
+
// Find the field
|
|
372
|
+
let field;
|
|
373
|
+
if (resolvedStructure.type === 'sequence') {
|
|
374
|
+
field = resolvedStructure.fields.find(f => f.name === fieldName);
|
|
375
|
+
} else if (resolvedStructure.type === 'delimited' && zoneName) {
|
|
376
|
+
const zone = resolvedStructure.zones.find(z => z.name === zoneName);
|
|
377
|
+
field = zone?.fields.find(f => f.name === fieldName);
|
|
378
|
+
}
|
|
379
|
+
if (!field || nodeIndex < 0 || nodeIndex >= field.nodes.length) return;
|
|
380
|
+
|
|
381
|
+
const targetNode = field.nodes[nodeIndex];
|
|
382
|
+
if (targetNode.type !== 'rune') return;
|
|
383
|
+
|
|
384
|
+
// Find this node's index in the effectiveContentTree
|
|
385
|
+
const tree = effectiveContentTree;
|
|
386
|
+
const treeIndex = tree.findIndex(n => n.source === targetNode.source);
|
|
387
|
+
if (treeIndex === -1) return;
|
|
388
|
+
|
|
389
|
+
// Navigate to the nested rune
|
|
390
|
+
if (activeNode?.type === 'rune') {
|
|
391
|
+
activePath = [...activePath, treeIndex];
|
|
392
|
+
} else {
|
|
393
|
+
activePath = [treeIndex];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Switch to settings tab to show the nested rune's settings
|
|
397
|
+
activeTab = 'settings';
|
|
398
|
+
}
|
|
399
|
+
|
|
117
400
|
// ── Edit handlers ────────────────────────────────────────────
|
|
118
401
|
|
|
119
402
|
function handleHeadingTextChange(text: string) {
|
|
@@ -241,27 +524,13 @@
|
|
|
241
524
|
|
|
242
525
|
</script>
|
|
243
526
|
|
|
244
|
-
<svelte:window onmousedown={handleTreeClickOutside} onkeydown={handleTreeKeydown} />
|
|
245
|
-
|
|
246
527
|
<div class="edit-panel">
|
|
247
|
-
<div class="edit-
|
|
528
|
+
<div class="edit-panel__top">
|
|
248
529
|
<div class="edit-panel__header">
|
|
249
530
|
<span class="edit-panel__type">{headerLabel}</span>
|
|
250
531
|
{#if !activeIsContent && category}
|
|
251
532
|
<span class="edit-panel__category">{category}</span>
|
|
252
533
|
{/if}
|
|
253
|
-
{#if hasNestedRunes}
|
|
254
|
-
<button
|
|
255
|
-
class="edit-panel__btn"
|
|
256
|
-
class:active={showTreeDropdown}
|
|
257
|
-
onclick={toggleTreeDropdown}
|
|
258
|
-
title="Content tree"
|
|
259
|
-
>
|
|
260
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
261
|
-
<path d="M2 3h4M2 7h4M6 11h4M6 15h4M4 3v8M8 11v4" />
|
|
262
|
-
</svg>
|
|
263
|
-
</button>
|
|
264
|
-
{/if}
|
|
265
534
|
<div class="edit-panel__spacer"></div>
|
|
266
535
|
<button
|
|
267
536
|
class="edit-panel__btn edit-panel__btn--danger"
|
|
@@ -283,176 +552,217 @@
|
|
|
283
552
|
>×</button>
|
|
284
553
|
</div>
|
|
285
554
|
|
|
286
|
-
{#if
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
555
|
+
{#if block.type === 'rune' && availableTabs.length > 1}
|
|
556
|
+
<div class="edit-panel__tabs">
|
|
557
|
+
{#each availableTabs as tab}
|
|
558
|
+
<button
|
|
559
|
+
type="button"
|
|
560
|
+
class="edit-panel__tab"
|
|
561
|
+
class:active={activeTab === tab}
|
|
562
|
+
onclick={() => activeTab = tab}
|
|
563
|
+
>
|
|
564
|
+
{#if tab === 'settings'}
|
|
565
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
566
|
+
<circle cx="8" cy="8" r="2" />
|
|
567
|
+
<path d="M6.7 1.6h2.6l.4 1.8.8.4 1.7-.7 1.8 1.8-.7 1.7.4.8 1.8.4v2.6l-1.8.4-.4.8.7 1.7-1.8 1.8-1.7-.7-.8.4-.4 1.8H6.7l-.4-1.8-.8-.4-1.7.7-1.8-1.8.7-1.7-.4-.8-1.8-.4V6.7l1.8-.4.4-.8-.7-1.7 1.8-1.8 1.7.7.8-.4z" />
|
|
568
|
+
</svg>
|
|
569
|
+
Settings
|
|
570
|
+
{:else if tab === 'structure'}
|
|
571
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
572
|
+
<path d="M2 3h4M2 7h4M6 11h4M6 15h4M4 3v8M8 11v4" />
|
|
573
|
+
</svg>
|
|
574
|
+
Structure
|
|
575
|
+
{:else if tab === 'content'}
|
|
576
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
577
|
+
<path d="M2 4h12M2 8h12M2 12h8" />
|
|
578
|
+
</svg>
|
|
579
|
+
Content
|
|
580
|
+
{/if}
|
|
581
|
+
</button>
|
|
582
|
+
{/each}
|
|
297
583
|
</div>
|
|
298
584
|
{/if}
|
|
299
585
|
</div>
|
|
300
586
|
|
|
301
|
-
|
|
302
|
-
{
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
587
|
+
{#if block.type === 'rune'}
|
|
588
|
+
{@const rb = block as RuneBlock}
|
|
589
|
+
|
|
590
|
+
<!-- Settings tab -->
|
|
591
|
+
{#if activeTab === 'settings'}
|
|
592
|
+
<div class="edit-panel__tab-panel">
|
|
593
|
+
{#if activeIsContent && activeNode}
|
|
594
|
+
{#if activeNode.type === 'heading'}
|
|
595
|
+
<div class="edit-panel__field-group">
|
|
596
|
+
<label class="edit-panel__field">
|
|
597
|
+
<span class="edit-panel__field-label">Level</span>
|
|
598
|
+
<select
|
|
599
|
+
class="edit-panel__select"
|
|
600
|
+
value={String(activeNode.headingLevel ?? 1)}
|
|
601
|
+
onchange={(e) => handleNestedHeadingLevelChange(Number((e.target as HTMLSelectElement).value))}
|
|
602
|
+
>
|
|
603
|
+
<option value="1">H1</option>
|
|
604
|
+
<option value="2">H2</option>
|
|
605
|
+
<option value="3">H3</option>
|
|
606
|
+
<option value="4">H4</option>
|
|
607
|
+
<option value="5">H5</option>
|
|
608
|
+
<option value="6">H6</option>
|
|
609
|
+
</select>
|
|
610
|
+
</label>
|
|
611
|
+
<label class="edit-panel__field">
|
|
612
|
+
<span class="edit-panel__field-label">Text</span>
|
|
613
|
+
<input
|
|
614
|
+
class="edit-panel__input"
|
|
615
|
+
type="text"
|
|
616
|
+
value={activeNode.headingText ?? ''}
|
|
617
|
+
oninput={(e) => handleNestedHeadingTextChange((e.target as HTMLInputElement).value)}
|
|
618
|
+
/>
|
|
619
|
+
</label>
|
|
620
|
+
</div>
|
|
621
|
+
{:else if activeNode.type === 'fence'}
|
|
622
|
+
<div class="edit-panel__field-group">
|
|
623
|
+
<label class="edit-panel__field">
|
|
624
|
+
<span class="edit-panel__field-label">Language</span>
|
|
625
|
+
<input
|
|
626
|
+
class="edit-panel__input"
|
|
627
|
+
type="text"
|
|
628
|
+
value={activeNode.fenceLanguage ?? ''}
|
|
629
|
+
oninput={(e) => handleNestedFenceLangChange((e.target as HTMLInputElement).value)}
|
|
630
|
+
placeholder="e.g. js, python, html"
|
|
631
|
+
/>
|
|
632
|
+
</label>
|
|
633
|
+
</div>
|
|
634
|
+
{:else}
|
|
635
|
+
<div class="edit-panel__empty-tab">
|
|
636
|
+
<span class="edit-panel__empty-tab-text">No settings for this element</span>
|
|
637
|
+
</div>
|
|
638
|
+
{/if}
|
|
639
|
+
|
|
640
|
+
{:else if activeNode && activeRuneInfo}
|
|
641
|
+
<RuneAttributes
|
|
642
|
+
runeInfo={activeRuneInfo}
|
|
643
|
+
attributes={activeNode.attributes ?? {}}
|
|
644
|
+
onchange={handleNestedAttrsChange}
|
|
327
645
|
/>
|
|
328
|
-
</label>
|
|
329
|
-
</div>
|
|
330
646
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
{#if activeNode.type === 'heading'}
|
|
338
|
-
<div class="edit-panel__field-group">
|
|
339
|
-
<label class="edit-panel__field">
|
|
340
|
-
<span class="edit-panel__field-label">Level</span>
|
|
341
|
-
<select
|
|
342
|
-
class="edit-panel__select"
|
|
343
|
-
value={String(activeNode.headingLevel ?? 1)}
|
|
344
|
-
onchange={(e) => handleNestedHeadingLevelChange(Number((e.target as HTMLSelectElement).value))}
|
|
345
|
-
>
|
|
346
|
-
<option value="1">H1</option>
|
|
347
|
-
<option value="2">H2</option>
|
|
348
|
-
<option value="3">H3</option>
|
|
349
|
-
<option value="4">H4</option>
|
|
350
|
-
<option value="5">H5</option>
|
|
351
|
-
<option value="6">H6</option>
|
|
352
|
-
</select>
|
|
353
|
-
</label>
|
|
354
|
-
<label class="edit-panel__field">
|
|
355
|
-
<span class="edit-panel__field-label">Text</span>
|
|
356
|
-
<input
|
|
357
|
-
class="edit-panel__input"
|
|
358
|
-
type="text"
|
|
359
|
-
value={activeNode.headingText ?? ''}
|
|
360
|
-
oninput={(e) => handleNestedHeadingTextChange((e.target as HTMLInputElement).value)}
|
|
361
|
-
/>
|
|
362
|
-
</label>
|
|
363
|
-
</div>
|
|
364
|
-
{:else if activeNode.type === 'fence'}
|
|
365
|
-
<div class="edit-panel__field-group">
|
|
366
|
-
<label class="edit-panel__field">
|
|
367
|
-
<span class="edit-panel__field-label">Language</span>
|
|
368
|
-
<input
|
|
369
|
-
class="edit-panel__input"
|
|
370
|
-
type="text"
|
|
371
|
-
value={activeNode.fenceLanguage ?? ''}
|
|
372
|
-
oninput={(e) => handleNestedFenceLangChange((e.target as HTMLInputElement).value)}
|
|
373
|
-
placeholder="e.g. js, python, html"
|
|
374
|
-
/>
|
|
375
|
-
</label>
|
|
376
|
-
</div>
|
|
377
|
-
<div class="edit-panel__content-editor">
|
|
378
|
-
<InlineEditor
|
|
379
|
-
content={activeNode.fenceCode ?? ''}
|
|
380
|
-
onchange={handleNestedFenceCodeChange}
|
|
381
|
-
language={activeNode.fenceLanguage}
|
|
382
|
-
{runes}
|
|
383
|
-
aggregated={() => aggregated}
|
|
384
|
-
/>
|
|
385
|
-
</div>
|
|
386
|
-
{:else if activeNode.type === 'paragraph'}
|
|
387
|
-
<div class="edit-panel__content-editor">
|
|
388
|
-
<InlineEditor
|
|
389
|
-
content={activeNode.source}
|
|
390
|
-
onchange={handleNestedSourceChange}
|
|
391
|
-
{runes}
|
|
392
|
-
aggregated={() => aggregated}
|
|
647
|
+
{:else if !activeNode}
|
|
648
|
+
{#if runeInfo}
|
|
649
|
+
<RuneAttributes
|
|
650
|
+
{runeInfo}
|
|
651
|
+
attributes={rb.attributes}
|
|
652
|
+
onchange={handleRuneAttrsChange}
|
|
393
653
|
/>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
654
|
+
{:else}
|
|
655
|
+
<div class="edit-panel__unknown">
|
|
656
|
+
<span class="edit-panel__unknown-label">Unknown rune: {rb.runeName}</span>
|
|
657
|
+
{#each Object.entries(rb.attributes) as [key, val]}
|
|
658
|
+
<label class="edit-panel__field">
|
|
659
|
+
<span class="edit-panel__field-label">{key}</span>
|
|
660
|
+
<input
|
|
661
|
+
class="edit-panel__input"
|
|
662
|
+
type="text"
|
|
663
|
+
value={val}
|
|
664
|
+
oninput={(e) => {
|
|
665
|
+
const next = { ...rb.attributes, [key]: (e.target as HTMLInputElement).value };
|
|
666
|
+
handleRuneAttrsChange(next);
|
|
667
|
+
}}
|
|
668
|
+
/>
|
|
669
|
+
</label>
|
|
670
|
+
{/each}
|
|
671
|
+
</div>
|
|
672
|
+
{/if}
|
|
408
673
|
{/if}
|
|
674
|
+
</div>
|
|
675
|
+
{/if}
|
|
409
676
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
677
|
+
<!-- Structure tab -->
|
|
678
|
+
{#if activeTab === 'structure'}
|
|
679
|
+
<div class="edit-panel__tab-panel">
|
|
680
|
+
{#if hasContentModel && resolvedStructure}
|
|
681
|
+
<ContentModelTree
|
|
682
|
+
structure={resolvedStructure}
|
|
683
|
+
rootLabel={activeNode?.runeName ?? rb.runeName}
|
|
684
|
+
onaddfield={handleAddField}
|
|
685
|
+
onremovefield={handleRemoveField}
|
|
686
|
+
onappenditem={handleAppendItem}
|
|
687
|
+
onremovelistitem={handleRemoveListItem}
|
|
688
|
+
onreorderlistitem={handleReorderListItem}
|
|
689
|
+
oneditfield={handleEditField}
|
|
690
|
+
oneditlistitem={handleEditListItem}
|
|
691
|
+
onnavigaterune={handleNavigateRune}
|
|
692
|
+
onfieldselect={handleFieldSelect}
|
|
693
|
+
{selectedField}
|
|
694
|
+
/>
|
|
695
|
+
{:else if !activeNode && hasNestedRunes}
|
|
696
|
+
<ContentTree
|
|
697
|
+
nodes={contentTree}
|
|
698
|
+
{activePath}
|
|
699
|
+
onselect={handleTreeSelect}
|
|
700
|
+
rootLabel={rb.runeName}
|
|
701
|
+
onrootclick={navigateToRoot}
|
|
702
|
+
isRootActive={activePath.length === 0}
|
|
703
|
+
/>
|
|
426
704
|
{/if}
|
|
705
|
+
</div>
|
|
706
|
+
{/if}
|
|
427
707
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
708
|
+
<!-- Content tab -->
|
|
709
|
+
{#if activeTab === 'content' && !rb.selfClosing}
|
|
710
|
+
<div class="edit-panel__tab-panel">
|
|
711
|
+
{#if activeIsContent && activeNode}
|
|
712
|
+
{#if activeNode.type === 'fence'}
|
|
713
|
+
<div class="edit-panel__content-editor">
|
|
714
|
+
<InlineEditor
|
|
715
|
+
content={activeNode.fenceCode ?? ''}
|
|
716
|
+
onchange={handleNestedFenceCodeChange}
|
|
717
|
+
language={activeNode.fenceLanguage}
|
|
718
|
+
{runes}
|
|
719
|
+
aggregated={() => aggregated}
|
|
720
|
+
/>
|
|
721
|
+
</div>
|
|
722
|
+
{:else if activeNode.type === 'paragraph'}
|
|
723
|
+
<div class="edit-panel__content-editor">
|
|
724
|
+
<InlineEditor
|
|
725
|
+
content={activeNode.source}
|
|
726
|
+
onchange={handleNestedSourceChange}
|
|
727
|
+
{runes}
|
|
728
|
+
aggregated={() => aggregated}
|
|
729
|
+
/>
|
|
730
|
+
</div>
|
|
731
|
+
{:else if activeNode.type === 'heading'}
|
|
732
|
+
<div class="edit-panel__empty-tab">
|
|
733
|
+
<span class="edit-panel__empty-tab-text">Edit heading text in Settings</span>
|
|
734
|
+
</div>
|
|
735
|
+
{:else}
|
|
736
|
+
<div class="edit-panel__field-group">
|
|
440
737
|
<label class="edit-panel__field">
|
|
441
|
-
<span class="edit-panel__field-label">
|
|
442
|
-
<
|
|
443
|
-
class="edit-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
handleRuneAttrsChange(next);
|
|
449
|
-
}}
|
|
450
|
-
/>
|
|
738
|
+
<span class="edit-panel__field-label">Source</span>
|
|
739
|
+
<textarea
|
|
740
|
+
class="edit-panel__textarea"
|
|
741
|
+
value={activeNode.source}
|
|
742
|
+
oninput={(e) => handleNestedSourceChange((e.target as HTMLTextAreaElement).value)}
|
|
743
|
+
rows={Math.max(4, activeNode.source.split('\n').length)}
|
|
744
|
+
></textarea>
|
|
451
745
|
</label>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
{
|
|
746
|
+
</div>
|
|
747
|
+
{/if}
|
|
748
|
+
|
|
749
|
+
{:else if activeNode && activeRuneInfo}
|
|
750
|
+
{#if !activeNode.selfClosing && activeNode.innerContent !== undefined}
|
|
751
|
+
<div class="edit-panel__content-editor">
|
|
752
|
+
<InlineEditor
|
|
753
|
+
content={activeNode.innerContent}
|
|
754
|
+
onchange={handleNestedContentChange}
|
|
755
|
+
{runes}
|
|
756
|
+
aggregated={() => aggregated}
|
|
757
|
+
/>
|
|
758
|
+
</div>
|
|
759
|
+
{:else}
|
|
760
|
+
<div class="edit-panel__empty-tab">
|
|
761
|
+
<span class="edit-panel__empty-tab-text">This rune has no inner content</span>
|
|
762
|
+
</div>
|
|
763
|
+
{/if}
|
|
764
|
+
|
|
765
|
+
{:else if !activeNode}
|
|
456
766
|
<div class="edit-panel__content-editor">
|
|
457
767
|
<InlineEditor
|
|
458
768
|
content={rb.innerContent}
|
|
@@ -462,64 +772,104 @@
|
|
|
462
772
|
/>
|
|
463
773
|
</div>
|
|
464
774
|
{/if}
|
|
465
|
-
{/if}
|
|
466
|
-
|
|
467
|
-
{:else if block.type === 'fence'}
|
|
468
|
-
{@const fb = block as FenceBlock}
|
|
469
|
-
<div class="edit-panel__field-group">
|
|
470
|
-
<label class="edit-panel__field">
|
|
471
|
-
<span class="edit-panel__field-label">Language</span>
|
|
472
|
-
<input
|
|
473
|
-
class="edit-panel__input"
|
|
474
|
-
type="text"
|
|
475
|
-
value={fb.language}
|
|
476
|
-
oninput={(e) => handleFenceLangChange((e.target as HTMLInputElement).value)}
|
|
477
|
-
placeholder="e.g. js, python, html"
|
|
478
|
-
/>
|
|
479
|
-
</label>
|
|
480
|
-
</div>
|
|
481
|
-
<div class="edit-panel__content-editor">
|
|
482
|
-
<InlineEditor
|
|
483
|
-
content={fb.code}
|
|
484
|
-
onchange={handleFenceCodeChange}
|
|
485
|
-
language={fb.language}
|
|
486
|
-
{runes}
|
|
487
|
-
/>
|
|
488
|
-
aggregated={() => aggregated}
|
|
489
|
-
</div>
|
|
490
|
-
|
|
491
|
-
{:else if block.type === 'paragraph'}
|
|
492
|
-
<div class="edit-panel__content-editor">
|
|
493
|
-
<InlineEditor
|
|
494
|
-
content={block.source}
|
|
495
|
-
onchange={handleSourceChange}
|
|
496
|
-
{runes}
|
|
497
|
-
/>
|
|
498
|
-
aggregated={() => aggregated}
|
|
499
|
-
</div>
|
|
500
|
-
|
|
501
|
-
{:else}
|
|
502
|
-
<!-- List, quote, image, etc. — raw source editing -->
|
|
503
|
-
<div class="edit-panel__field-group">
|
|
504
|
-
<label class="edit-panel__field">
|
|
505
|
-
<span class="edit-panel__field-label">Source</span>
|
|
506
|
-
<textarea
|
|
507
|
-
class="edit-panel__textarea"
|
|
508
|
-
value={block.source}
|
|
509
|
-
oninput={(e) => handleSourceChange((e.target as HTMLTextAreaElement).value)}
|
|
510
|
-
rows={Math.max(4, block.source.split('\n').length)}
|
|
511
|
-
></textarea>
|
|
512
|
-
</label>
|
|
513
775
|
</div>
|
|
514
776
|
{/if}
|
|
515
|
-
|
|
777
|
+
|
|
778
|
+
{:else}
|
|
779
|
+
<div class="edit-panel__body">
|
|
780
|
+
{#if block.type === 'heading'}
|
|
781
|
+
{@const hb = block as HeadingBlock}
|
|
782
|
+
<div class="edit-panel__field-group">
|
|
783
|
+
<label class="edit-panel__field">
|
|
784
|
+
<span class="edit-panel__field-label">Level</span>
|
|
785
|
+
<select
|
|
786
|
+
class="edit-panel__select"
|
|
787
|
+
value={String(hb.level)}
|
|
788
|
+
onchange={(e) => handleHeadingLevelChange(Number((e.target as HTMLSelectElement).value))}
|
|
789
|
+
>
|
|
790
|
+
<option value="1">H1</option>
|
|
791
|
+
<option value="2">H2</option>
|
|
792
|
+
<option value="3">H3</option>
|
|
793
|
+
<option value="4">H4</option>
|
|
794
|
+
<option value="5">H5</option>
|
|
795
|
+
<option value="6">H6</option>
|
|
796
|
+
</select>
|
|
797
|
+
</label>
|
|
798
|
+
<label class="edit-panel__field">
|
|
799
|
+
<span class="edit-panel__field-label">Text</span>
|
|
800
|
+
<input
|
|
801
|
+
class="edit-panel__input"
|
|
802
|
+
type="text"
|
|
803
|
+
value={hb.text}
|
|
804
|
+
oninput={(e) => handleHeadingTextChange((e.target as HTMLInputElement).value)}
|
|
805
|
+
/>
|
|
806
|
+
</label>
|
|
807
|
+
</div>
|
|
808
|
+
|
|
809
|
+
{:else if block.type === 'fence'}
|
|
810
|
+
{@const fb = block as FenceBlock}
|
|
811
|
+
<div class="edit-panel__field-group">
|
|
812
|
+
<label class="edit-panel__field">
|
|
813
|
+
<span class="edit-panel__field-label">Language</span>
|
|
814
|
+
<input
|
|
815
|
+
class="edit-panel__input"
|
|
816
|
+
type="text"
|
|
817
|
+
value={fb.language}
|
|
818
|
+
oninput={(e) => handleFenceLangChange((e.target as HTMLInputElement).value)}
|
|
819
|
+
placeholder="e.g. js, python, html"
|
|
820
|
+
/>
|
|
821
|
+
</label>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="edit-panel__content-editor">
|
|
824
|
+
<InlineEditor
|
|
825
|
+
content={fb.code}
|
|
826
|
+
onchange={handleFenceCodeChange}
|
|
827
|
+
language={fb.language}
|
|
828
|
+
{runes}
|
|
829
|
+
aggregated={() => aggregated}
|
|
830
|
+
/>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
{:else if block.type === 'paragraph'}
|
|
834
|
+
<div class="edit-panel__content-editor">
|
|
835
|
+
<InlineEditor
|
|
836
|
+
content={block.source}
|
|
837
|
+
onchange={handleSourceChange}
|
|
838
|
+
{runes}
|
|
839
|
+
aggregated={() => aggregated}
|
|
840
|
+
/>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
{:else}
|
|
844
|
+
<!-- List, quote, image, etc. — raw source editing -->
|
|
845
|
+
<div class="edit-panel__field-group">
|
|
846
|
+
<label class="edit-panel__field">
|
|
847
|
+
<span class="edit-panel__field-label">Source</span>
|
|
848
|
+
<textarea
|
|
849
|
+
class="edit-panel__textarea"
|
|
850
|
+
value={block.source}
|
|
851
|
+
oninput={(e) => handleSourceChange((e.target as HTMLTextAreaElement).value)}
|
|
852
|
+
rows={Math.max(4, block.source.split('\n').length)}
|
|
853
|
+
></textarea>
|
|
854
|
+
</label>
|
|
855
|
+
</div>
|
|
856
|
+
{/if}
|
|
857
|
+
</div>
|
|
858
|
+
{/if}
|
|
516
859
|
</div>
|
|
517
860
|
|
|
518
861
|
<style>
|
|
519
862
|
.edit-panel {
|
|
520
863
|
display: flex;
|
|
521
864
|
flex-direction: column;
|
|
522
|
-
|
|
865
|
+
flex: 1;
|
|
866
|
+
min-height: 0;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.edit-panel__top {
|
|
870
|
+
flex-shrink: 0;
|
|
871
|
+
background: var(--ed-surface-0);
|
|
872
|
+
border-bottom: 1px solid var(--ed-border-default);
|
|
523
873
|
}
|
|
524
874
|
|
|
525
875
|
.edit-panel__header {
|
|
@@ -527,11 +877,6 @@
|
|
|
527
877
|
align-items: center;
|
|
528
878
|
gap: 0.5rem;
|
|
529
879
|
padding: var(--ed-space-4) var(--ed-space-5);
|
|
530
|
-
border-bottom: 1px solid var(--ed-border-default);
|
|
531
|
-
background: transparent;
|
|
532
|
-
position: sticky;
|
|
533
|
-
top: 0;
|
|
534
|
-
z-index: 1;
|
|
535
880
|
}
|
|
536
881
|
|
|
537
882
|
.edit-panel__type {
|
|
@@ -672,9 +1017,10 @@
|
|
|
672
1017
|
.edit-panel__content-editor {
|
|
673
1018
|
display: flex;
|
|
674
1019
|
flex-direction: column;
|
|
1020
|
+
flex-shrink: 0;
|
|
675
1021
|
overflow: hidden;
|
|
676
|
-
margin-left: calc(-1 * var(--ed-space-
|
|
677
|
-
margin-right: calc(-1 * var(--ed-space-
|
|
1022
|
+
margin-left: calc(-1 * var(--ed-space-5));
|
|
1023
|
+
margin-right: calc(-1 * var(--ed-space-5));
|
|
678
1024
|
border-top: 1px solid var(--ed-border-subtle);
|
|
679
1025
|
}
|
|
680
1026
|
|
|
@@ -690,30 +1036,64 @@
|
|
|
690
1036
|
font-style: italic;
|
|
691
1037
|
}
|
|
692
1038
|
|
|
693
|
-
/*
|
|
694
|
-
.edit-
|
|
695
|
-
|
|
696
|
-
|
|
1039
|
+
/* Tab strip */
|
|
1040
|
+
.edit-panel__tabs {
|
|
1041
|
+
display: flex;
|
|
1042
|
+
gap: 2px;
|
|
1043
|
+
background: var(--ed-surface-2);
|
|
1044
|
+
border-radius: var(--ed-radius-sm);
|
|
1045
|
+
padding: 2px;
|
|
1046
|
+
margin: 0 var(--ed-space-4) var(--ed-space-3);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.edit-panel__tab {
|
|
1050
|
+
flex: 1;
|
|
1051
|
+
display: flex;
|
|
1052
|
+
align-items: center;
|
|
1053
|
+
justify-content: center;
|
|
1054
|
+
gap: 0.35rem;
|
|
1055
|
+
padding: 0.35rem 0.5rem;
|
|
1056
|
+
border: none;
|
|
1057
|
+
background: transparent;
|
|
1058
|
+
color: var(--ed-text-muted);
|
|
1059
|
+
font-size: var(--ed-text-sm);
|
|
1060
|
+
font-weight: 500;
|
|
1061
|
+
cursor: pointer;
|
|
1062
|
+
border-radius: calc(var(--ed-radius-sm) - 1px);
|
|
1063
|
+
transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
|
|
697
1064
|
}
|
|
698
1065
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
color: var(--ed-accent);
|
|
702
|
-
background: var(--ed-accent-muted);
|
|
1066
|
+
.edit-panel__tab:hover {
|
|
1067
|
+
color: var(--ed-text-secondary);
|
|
703
1068
|
}
|
|
704
1069
|
|
|
705
|
-
|
|
706
|
-
.edit-panel__tree-dropdown {
|
|
707
|
-
padding: 0.5rem;
|
|
708
|
-
border-bottom: 1px solid var(--ed-border-default);
|
|
1070
|
+
.edit-panel__tab.active {
|
|
709
1071
|
background: var(--ed-surface-0);
|
|
710
|
-
|
|
1072
|
+
color: var(--ed-text-primary);
|
|
1073
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/* Tab panels */
|
|
1077
|
+
.edit-panel__tab-panel {
|
|
1078
|
+
flex: 1;
|
|
711
1079
|
overflow-y: auto;
|
|
712
|
-
|
|
1080
|
+
padding: var(--ed-space-5);
|
|
1081
|
+
display: flex;
|
|
1082
|
+
flex-direction: column;
|
|
1083
|
+
gap: var(--ed-space-5);
|
|
713
1084
|
}
|
|
714
1085
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1086
|
+
/* Empty tab state */
|
|
1087
|
+
.edit-panel__empty-tab {
|
|
1088
|
+
display: flex;
|
|
1089
|
+
align-items: center;
|
|
1090
|
+
justify-content: center;
|
|
1091
|
+
padding: var(--ed-space-8) var(--ed-space-4);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.edit-panel__empty-tab-text {
|
|
1095
|
+
font-size: var(--ed-text-sm);
|
|
1096
|
+
color: var(--ed-text-muted);
|
|
1097
|
+
font-style: italic;
|
|
718
1098
|
}
|
|
719
1099
|
</style>
|