@refrakt-md/editor 0.7.2 → 0.8.1
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-4SP4_AaD.js → index-BBinZAiy.js} +1 -1
- package/app/dist/assets/index-BD2EBUrQ.css +1 -0
- package/app/dist/assets/{index-D77rckeh.js → index-BLuaHLN3.js} +1 -1
- package/app/dist/assets/{index-30gAspk8.js → index-BgCNqcSo.js} +1 -1
- package/app/dist/assets/index-BlAOhWAQ.js +453 -0
- package/app/dist/assets/{index-BZ4adnS0.js → index-BwFn9q4x.js} +1 -1
- package/app/dist/assets/{index-DFkteo0w.js → index-C72UC2ga.js} +1 -1
- package/app/dist/assets/{index-x67KGOIr.js → index-COIPZ34u.js} +1 -1
- package/app/dist/assets/{index-BEFUVB_B.js → index-CW02bulk.js} +1 -1
- package/app/dist/assets/{index-CI5PewQM.js → index-CXFMPmtf.js} +1 -1
- package/app/dist/assets/{index-ByHhigzw.js → index-CeU_s7BB.js} +1 -1
- package/app/dist/assets/{index-DvgOtlru.js → index-CqHjo2YT.js} +1 -1
- package/app/dist/assets/{index-DKnhR16N.js → index-D3TQo8gu.js} +1 -1
- package/app/dist/assets/{index-Baf7ZSct.js → index-DVM3uoxc.js} +1 -1
- package/app/dist/assets/{index-C9w1RpYY.js → index-DW2zI-Ss.js} +1 -1
- package/app/dist/assets/{index--rGC9bba.js → index-D_Y6J00B.js} +1 -1
- package/app/dist/assets/{index-kPhFxtn-.js → index-DgIg-QAA.js} +2 -2
- package/app/dist/assets/{index-DIuFNfTc.js → index-DmY6uqAw.js} +1 -1
- package/app/dist/assets/{index-D1WOi3EN.js → index-DzHt8ZRh.js} +1 -1
- package/app/dist/assets/{index-BwWzfQVn.js → index-ZLvRNfLb.js} +1 -1
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +49 -0
- package/app/src/lib/components/ActionEditPopover.svelte +245 -0
- package/app/src/lib/components/BlockCard.svelte +255 -1
- package/app/src/lib/components/BlockEditPanel.svelte +697 -138
- package/app/src/lib/components/BlockEditor.svelte +467 -389
- package/app/src/lib/components/CodeEditPopover.svelte +226 -0
- package/app/src/lib/components/ContentModelTree.svelte +562 -0
- package/app/src/lib/components/ContentTree.svelte +181 -0
- package/app/src/lib/components/EditorLayout.svelte +1 -6
- package/app/src/lib/components/FrontmatterEditPanel.svelte +0 -1
- package/app/src/lib/components/HeaderBar.svelte +38 -0
- package/app/src/lib/components/InlineEditPopover.svelte +593 -0
- package/app/src/lib/components/InsertBlockDialog.svelte +429 -0
- package/app/src/lib/components/PageCard.svelte +3 -4
- package/app/src/lib/components/PreviewPane.svelte +19 -1
- package/app/src/lib/components/RuneAttributes.svelte +249 -100
- package/app/src/lib/editor/block-parser.ts +463 -0
- package/app/src/lib/preview/block-renderer.ts +30 -14
- package/dist/community-tags-builder.d.ts.map +1 -1
- package/dist/community-tags-builder.js +5 -1
- package/dist/community-tags-builder.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +92 -6
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/preview-runtime/App.svelte +2 -0
- package/app/dist/assets/index-DlrXwdpb.css +0 -1
- package/app/dist/assets/index-GlUHQ_jL.js +0 -324
|
@@ -5,14 +5,27 @@
|
|
|
5
5
|
HeadingBlock,
|
|
6
6
|
RuneBlock,
|
|
7
7
|
FenceBlock,
|
|
8
|
+
ContentNode,
|
|
8
9
|
} from '../editor/block-parser.js';
|
|
9
10
|
import {
|
|
10
11
|
rebuildRuneSource,
|
|
11
12
|
rebuildHeadingSource,
|
|
12
13
|
rebuildFenceSource,
|
|
13
14
|
blockLabel,
|
|
15
|
+
parseContentTree,
|
|
16
|
+
serializeAttributes,
|
|
17
|
+
replaceNodeSource,
|
|
18
|
+
insertFieldContent,
|
|
19
|
+
removeFieldContent,
|
|
20
|
+
appendListItem,
|
|
21
|
+
removeListItem,
|
|
14
22
|
} from '../editor/block-parser.js';
|
|
23
|
+
import { resolveContentStructure } from '../editor/content-model-resolver.js';
|
|
24
|
+
import type { SectionMapping } from '../editor/section-mapper.js';
|
|
25
|
+
import { stripInlineMarkdown } from '../editor/inline-markdown.js';
|
|
15
26
|
import RuneAttributes from './RuneAttributes.svelte';
|
|
27
|
+
import ContentTree from './ContentTree.svelte';
|
|
28
|
+
import ContentModelTree from './ContentModelTree.svelte';
|
|
16
29
|
import InlineEditor from './InlineEditor.svelte';
|
|
17
30
|
|
|
18
31
|
interface Props {
|
|
@@ -20,12 +33,15 @@
|
|
|
20
33
|
runeMap: Map<string, RuneInfo>;
|
|
21
34
|
runes: () => RuneInfo[];
|
|
22
35
|
aggregated?: Record<string, unknown>;
|
|
36
|
+
/** When set, auto-navigate to the Nth nested rune (DFS order) on mount */
|
|
37
|
+
initialRuneIndex?: number | null;
|
|
23
38
|
onupdate: (block: ParsedBlock) => void;
|
|
24
39
|
onremove: () => void;
|
|
25
40
|
onclose: () => void;
|
|
41
|
+
oneditfield?: (dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) => void;
|
|
26
42
|
}
|
|
27
43
|
|
|
28
|
-
let { block, runeMap, runes, aggregated = {}, onupdate, onremove, onclose }: Props = $props();
|
|
44
|
+
let { block, runeMap, runes, aggregated = {}, initialRuneIndex = null, onupdate, onremove, onclose, oneditfield }: Props = $props();
|
|
29
45
|
|
|
30
46
|
let label = $derived(blockLabel(block));
|
|
31
47
|
|
|
@@ -35,6 +51,225 @@
|
|
|
35
51
|
|
|
36
52
|
let category = $derived(runeInfo?.category ?? null);
|
|
37
53
|
|
|
54
|
+
// ── Nested rune tree ─────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Path of indices into the content tree for the selected nested rune */
|
|
57
|
+
let activePath: number[] = $state([]);
|
|
58
|
+
|
|
59
|
+
/** Parse inner content into a tree (for rune blocks with content) */
|
|
60
|
+
let contentTree = $derived.by(() => {
|
|
61
|
+
if (block.type !== 'rune') return [];
|
|
62
|
+
const rb = block as RuneBlock;
|
|
63
|
+
if (rb.selfClosing || !rb.innerContent.trim()) return [];
|
|
64
|
+
return parseContentTree(rb.innerContent);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** Whether this rune has any nested rune children worth showing a tree for */
|
|
68
|
+
let hasNestedRunes = $derived(
|
|
69
|
+
contentTree.some(n => n.type === 'rune')
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/** The effective rune info for the structure tab — nested rune if selected, root otherwise */
|
|
73
|
+
let effectiveRuneInfo = $derived.by(() => {
|
|
74
|
+
if (activeNode?.type === 'rune' && activeRuneInfo) return activeRuneInfo;
|
|
75
|
+
return runeInfo;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Content tree for the effective rune (nested rune's children or root's) */
|
|
79
|
+
let effectiveContentTree = $derived.by(() => {
|
|
80
|
+
if (activeNode?.type === 'rune' && activeNode.children) return activeNode.children;
|
|
81
|
+
return contentTree;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Whether the effective rune has a declarative content model */
|
|
85
|
+
let hasContentModel = $derived(effectiveRuneInfo?.contentModel != null);
|
|
86
|
+
|
|
87
|
+
/** Resolved structure: effective content tree matched against effective content model */
|
|
88
|
+
let resolvedStructure = $derived.by(() => {
|
|
89
|
+
if (!effectiveRuneInfo?.contentModel) return null;
|
|
90
|
+
return resolveContentStructure(effectiveContentTree, effectiveRuneInfo.contentModel);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** Currently selected field in the content model tree */
|
|
94
|
+
let selectedField: string | null = $state(null);
|
|
95
|
+
|
|
96
|
+
/** Find the path to the Nth rune node in DFS order */
|
|
97
|
+
function findRunePathByDfsIndex(nodes: ContentNode[], target: number): number[] | null {
|
|
98
|
+
let count = 0;
|
|
99
|
+
function walk(ns: ContentNode[], path: number[]): number[] | null {
|
|
100
|
+
for (let i = 0; i < ns.length; i++) {
|
|
101
|
+
if (ns[i].type === 'rune') {
|
|
102
|
+
if (count === target) return [...path, i];
|
|
103
|
+
count++;
|
|
104
|
+
const found = walk(ns[i].children ?? [], [...path, i]);
|
|
105
|
+
if (found) return found;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return walk(nodes, []);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Auto-navigate to a nested rune when initialRuneIndex is provided
|
|
114
|
+
$effect(() => {
|
|
115
|
+
if (initialRuneIndex != null && contentTree.length > 0) {
|
|
116
|
+
const path = findRunePathByDfsIndex(contentTree, initialRuneIndex);
|
|
117
|
+
if (path) activePath = path;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** Resolve the active nested node from the path */
|
|
122
|
+
function resolveNode(nodes: ContentNode[], path: number[]): ContentNode | null {
|
|
123
|
+
if (path.length === 0) return null;
|
|
124
|
+
const [head, ...rest] = path;
|
|
125
|
+
const node = nodes[head];
|
|
126
|
+
if (!node) return null;
|
|
127
|
+
if (rest.length === 0) return node;
|
|
128
|
+
return resolveNode(node.children ?? [], rest);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let activeNode = $derived(resolveNode(contentTree, activePath));
|
|
132
|
+
|
|
133
|
+
let activeRuneInfo = $derived(
|
|
134
|
+
activeNode?.runeName ? runeMap.get(activeNode.runeName) ?? null : null
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/** Whether the active node is a non-rune content node */
|
|
138
|
+
let activeIsContent = $derived(activeNode != null && activeNode.type !== 'rune');
|
|
139
|
+
|
|
140
|
+
/** Display label for header — show content node type when one is selected */
|
|
141
|
+
let headerLabel = $derived.by(() => {
|
|
142
|
+
if (!activeNode) return label;
|
|
143
|
+
if (activeNode.type === 'rune') return activeNode.runeName ?? label;
|
|
144
|
+
return activeNode.type;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
type TabId = 'settings' | 'structure' | 'content';
|
|
148
|
+
let activeTab: TabId = $state('settings');
|
|
149
|
+
|
|
150
|
+
let availableTabs = $derived.by(() => {
|
|
151
|
+
if (block.type !== 'rune') return [] as TabId[];
|
|
152
|
+
const rb = block as RuneBlock;
|
|
153
|
+
const tabs: TabId[] = ['settings'];
|
|
154
|
+
// Show structure tab when the effective rune has a content model,
|
|
155
|
+
// or when no nested rune is selected and root has nested runes (legacy tree)
|
|
156
|
+
if (hasContentModel || (!activeNode && hasNestedRunes)) tabs.push('structure');
|
|
157
|
+
if (!rb.selfClosing) tabs.push('content');
|
|
158
|
+
return tabs;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Auto-switch away from structure tab if it becomes unavailable
|
|
162
|
+
$effect(() => {
|
|
163
|
+
if (activeTab === 'structure' && !availableTabs.includes('structure')) {
|
|
164
|
+
activeTab = 'settings';
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
function handleTreeSelect(path: number[]) {
|
|
169
|
+
activePath = path;
|
|
170
|
+
// Auto-switch to Content tab when selecting a content node
|
|
171
|
+
const node = resolveNode(contentTree, path);
|
|
172
|
+
if (node && node.type !== 'rune') {
|
|
173
|
+
activeTab = 'content';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function navigateToRoot() {
|
|
178
|
+
activePath = [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Content model field handlers ─────────────────────────────
|
|
182
|
+
|
|
183
|
+
function handleFieldSelect(fieldName: string, zoneName?: string) {
|
|
184
|
+
selectedField = zoneName ? `${zoneName}.${fieldName}` : fieldName;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Apply a field content change — works for both root and nested runes */
|
|
188
|
+
function applyFieldChange(updater: (content: string) => string) {
|
|
189
|
+
const rb = block as RuneBlock;
|
|
190
|
+
if (activeNode?.type === 'rune' && activeNode.innerContent !== undefined) {
|
|
191
|
+
// Nested rune: update its inner content, then replace in root
|
|
192
|
+
const newNestedInner = updater(activeNode.innerContent);
|
|
193
|
+
if (newNestedInner === activeNode.innerContent) return;
|
|
194
|
+
const attrStr = serializeAttributes(activeNode.attributes ?? {});
|
|
195
|
+
const inner = newNestedInner.trim();
|
|
196
|
+
const newSource = inner
|
|
197
|
+
? `{% ${activeNode.runeName}${attrStr} %}\n${inner}\n{% /${activeNode.runeName} %}`
|
|
198
|
+
: `{% ${activeNode.runeName}${attrStr} %}\n\n{% /${activeNode.runeName} %}`;
|
|
199
|
+
const newRootInner = replaceNodeSource(rb.innerContent, activeNode.source, newSource);
|
|
200
|
+
const updated: RuneBlock = { ...rb, innerContent: newRootInner, source: '' };
|
|
201
|
+
updated.source = rebuildRuneSource(updated);
|
|
202
|
+
onupdate(updated);
|
|
203
|
+
} else {
|
|
204
|
+
// Root rune
|
|
205
|
+
const newInner = updater(rb.innerContent);
|
|
206
|
+
if (newInner === rb.innerContent) return;
|
|
207
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
208
|
+
updated.source = rebuildRuneSource(updated);
|
|
209
|
+
onupdate(updated);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleAddField(fieldName: string, zoneName?: string) {
|
|
214
|
+
if (!resolvedStructure) return;
|
|
215
|
+
applyFieldChange(content => insertFieldContent(content, resolvedStructure!, fieldName, zoneName));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handleRemoveField(fieldName: string, zoneName?: string) {
|
|
219
|
+
if (!resolvedStructure) return;
|
|
220
|
+
applyFieldChange(content => removeFieldContent(content, resolvedStructure!, fieldName, zoneName));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleAppendItem(fieldName: string, zoneName?: string) {
|
|
224
|
+
if (!resolvedStructure) return;
|
|
225
|
+
applyFieldChange(content => appendListItem(content, resolvedStructure!, fieldName, zoneName));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleRemoveListItem(fieldName: string, itemIndex: number, zoneName?: string) {
|
|
229
|
+
if (!resolvedStructure) return;
|
|
230
|
+
applyFieldChange(content => removeListItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleEditField(fieldName: string, rect: DOMRect, zoneName?: string) {
|
|
234
|
+
if (!resolvedStructure || !oneditfield) return;
|
|
235
|
+
// Find the field in the resolved structure
|
|
236
|
+
let field;
|
|
237
|
+
if (resolvedStructure.type === 'sequence') {
|
|
238
|
+
field = resolvedStructure.fields.find(f => f.name === fieldName);
|
|
239
|
+
} else if (resolvedStructure.type === 'delimited' && zoneName) {
|
|
240
|
+
const zone = resolvedStructure.zones.find(z => z.name === zoneName);
|
|
241
|
+
field = zone?.fields.find(f => f.name === fieldName);
|
|
242
|
+
}
|
|
243
|
+
if (!field || !field.filled || field.nodes.length !== 1) return;
|
|
244
|
+
|
|
245
|
+
const source = field.nodes[0].source;
|
|
246
|
+
const trimmed = source.trim();
|
|
247
|
+
|
|
248
|
+
// Strip markdown prefix (heading markers, blockquote markers)
|
|
249
|
+
let prefix = '';
|
|
250
|
+
let inlineContent = trimmed;
|
|
251
|
+
const headingMatch = trimmed.match(/^(#{1,6}\s+)(.*)/);
|
|
252
|
+
if (headingMatch) {
|
|
253
|
+
prefix = headingMatch[1];
|
|
254
|
+
inlineContent = headingMatch[2];
|
|
255
|
+
} else {
|
|
256
|
+
const quoteMatch = trimmed.match(/^(>\s*)(.*)/);
|
|
257
|
+
if (quoteMatch) {
|
|
258
|
+
prefix = quoteMatch[1];
|
|
259
|
+
inlineContent = quoteMatch[2];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const mapping: SectionMapping = {
|
|
264
|
+
dataName: fieldName,
|
|
265
|
+
text: stripInlineMarkdown(inlineContent),
|
|
266
|
+
source,
|
|
267
|
+
sourcePrefix: prefix,
|
|
268
|
+
inlineSource: inlineContent,
|
|
269
|
+
};
|
|
270
|
+
oneditfield(fieldName, inlineContent, rect, mapping);
|
|
271
|
+
}
|
|
272
|
+
|
|
38
273
|
// ── Edit handlers ────────────────────────────────────────────
|
|
39
274
|
|
|
40
275
|
function handleHeadingTextChange(text: string) {
|
|
@@ -65,6 +300,45 @@
|
|
|
65
300
|
onupdate(updated);
|
|
66
301
|
}
|
|
67
302
|
|
|
303
|
+
/** Handle attribute changes for a nested rune node */
|
|
304
|
+
function handleNestedAttrsChange(attrs: Record<string, string>) {
|
|
305
|
+
if (!activeNode || !activeNode.runeName) return;
|
|
306
|
+
const oldSource = activeNode.source;
|
|
307
|
+
// Rebuild the nested rune's source with new attributes
|
|
308
|
+
const attrStr = serializeAttributes(attrs);
|
|
309
|
+
let newSource: string;
|
|
310
|
+
if (activeNode.selfClosing) {
|
|
311
|
+
newSource = `{% ${activeNode.runeName}${attrStr} /%}`;
|
|
312
|
+
} else {
|
|
313
|
+
const inner = (activeNode.innerContent ?? '').trim();
|
|
314
|
+
newSource = inner
|
|
315
|
+
? `{% ${activeNode.runeName}${attrStr} %}\n${inner}\n{% /${activeNode.runeName} %}`
|
|
316
|
+
: `{% ${activeNode.runeName}${attrStr} %}\n\n{% /${activeNode.runeName} %}`;
|
|
317
|
+
}
|
|
318
|
+
// Replace in the top-level block's inner content
|
|
319
|
+
const rb = block as RuneBlock;
|
|
320
|
+
const newInner = replaceNodeSource(rb.innerContent, oldSource, newSource);
|
|
321
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
322
|
+
updated.source = rebuildRuneSource(updated);
|
|
323
|
+
onupdate(updated);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Handle inner content changes for a nested rune node */
|
|
327
|
+
function handleNestedContentChange(content: string) {
|
|
328
|
+
if (!activeNode || !activeNode.runeName) return;
|
|
329
|
+
const oldSource = activeNode.source;
|
|
330
|
+
const attrStr = serializeAttributes(activeNode.attributes ?? {});
|
|
331
|
+
const inner = content.trim();
|
|
332
|
+
const newSource = inner
|
|
333
|
+
? `{% ${activeNode.runeName}${attrStr} %}\n${inner}\n{% /${activeNode.runeName} %}`
|
|
334
|
+
: `{% ${activeNode.runeName}${attrStr} %}\n\n{% /${activeNode.runeName} %}`;
|
|
335
|
+
const rb = block as RuneBlock;
|
|
336
|
+
const newInner = replaceNodeSource(rb.innerContent, oldSource, newSource);
|
|
337
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
338
|
+
updated.source = rebuildRuneSource(updated);
|
|
339
|
+
onupdate(updated);
|
|
340
|
+
}
|
|
341
|
+
|
|
68
342
|
function handleFenceCodeChange(code: string) {
|
|
69
343
|
const b = block as FenceBlock;
|
|
70
344
|
const updated: FenceBlock = { ...b, code, source: '' };
|
|
@@ -82,171 +356,395 @@
|
|
|
82
356
|
function handleSourceChange(source: string) {
|
|
83
357
|
onupdate({ ...block, source });
|
|
84
358
|
}
|
|
359
|
+
|
|
360
|
+
// ── Nested content node edit handlers ────────────────────────
|
|
361
|
+
|
|
362
|
+
/** Replace a nested content node's source within the parent rune's inner content */
|
|
363
|
+
function replaceNestedSource(newSource: string) {
|
|
364
|
+
if (!activeNode) return;
|
|
365
|
+
const rb = block as RuneBlock;
|
|
366
|
+
const newInner = replaceNodeSource(rb.innerContent, activeNode.source, newSource);
|
|
367
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
368
|
+
updated.source = rebuildRuneSource(updated);
|
|
369
|
+
onupdate(updated);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function handleNestedHeadingLevelChange(level: number) {
|
|
373
|
+
if (!activeNode) return;
|
|
374
|
+
replaceNestedSource(`${'#'.repeat(level)} ${activeNode.headingText ?? ''}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function handleNestedHeadingTextChange(text: string) {
|
|
378
|
+
if (!activeNode) return;
|
|
379
|
+
replaceNestedSource(`${'#'.repeat(activeNode.headingLevel ?? 1)} ${text}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function handleNestedFenceLangChange(lang: string) {
|
|
383
|
+
if (!activeNode) return;
|
|
384
|
+
const code = activeNode.fenceCode ?? '';
|
|
385
|
+
replaceNestedSource(`\`\`\`${lang}\n${code}\n\`\`\``);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function handleNestedFenceCodeChange(code: string) {
|
|
389
|
+
if (!activeNode) return;
|
|
390
|
+
const lang = activeNode.fenceLanguage ?? '';
|
|
391
|
+
replaceNestedSource(`\`\`\`${lang}\n${code}\n\`\`\``);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function handleNestedSourceChange(source: string) {
|
|
395
|
+
replaceNestedSource(source);
|
|
396
|
+
}
|
|
397
|
+
|
|
85
398
|
</script>
|
|
86
399
|
|
|
87
400
|
<div class="edit-panel">
|
|
88
|
-
<div class="edit-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
401
|
+
<div class="edit-panel__top">
|
|
402
|
+
<div class="edit-panel__header">
|
|
403
|
+
<span class="edit-panel__type">{headerLabel}</span>
|
|
404
|
+
{#if !activeIsContent && category}
|
|
405
|
+
<span class="edit-panel__category">{category}</span>
|
|
406
|
+
{/if}
|
|
407
|
+
<div class="edit-panel__spacer"></div>
|
|
408
|
+
<button
|
|
409
|
+
class="edit-panel__btn edit-panel__btn--danger"
|
|
410
|
+
onclick={onremove}
|
|
411
|
+
title="Remove block"
|
|
412
|
+
>
|
|
413
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
414
|
+
<polyline points="3 6 3 14 13 14 13 6" />
|
|
415
|
+
<line x1="1" y1="4" x2="15" y2="4" />
|
|
416
|
+
<line x1="6" y1="2" x2="10" y2="2" />
|
|
417
|
+
<line x1="6" y1="8" x2="6" y2="12" />
|
|
418
|
+
<line x1="10" y1="8" x2="10" y2="12" />
|
|
419
|
+
</svg>
|
|
420
|
+
</button>
|
|
421
|
+
<button
|
|
422
|
+
class="edit-panel__btn"
|
|
423
|
+
onclick={onclose}
|
|
424
|
+
title="Close panel"
|
|
425
|
+
>×</button>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
{#if block.type === 'rune' && availableTabs.length > 1}
|
|
429
|
+
<div class="edit-panel__tabs">
|
|
430
|
+
{#each availableTabs as tab}
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
class="edit-panel__tab"
|
|
434
|
+
class:active={activeTab === tab}
|
|
435
|
+
onclick={() => activeTab = tab}
|
|
436
|
+
>
|
|
437
|
+
{#if tab === 'settings'}
|
|
438
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
439
|
+
<circle cx="8" cy="8" r="2" />
|
|
440
|
+
<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" />
|
|
441
|
+
</svg>
|
|
442
|
+
Settings
|
|
443
|
+
{:else if tab === 'structure'}
|
|
444
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
445
|
+
<path d="M2 3h4M2 7h4M6 11h4M6 15h4M4 3v8M8 11v4" />
|
|
446
|
+
</svg>
|
|
447
|
+
Structure
|
|
448
|
+
{:else if tab === 'content'}
|
|
449
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
450
|
+
<path d="M2 4h12M2 8h12M2 12h8" />
|
|
451
|
+
</svg>
|
|
452
|
+
Content
|
|
453
|
+
{/if}
|
|
454
|
+
</button>
|
|
455
|
+
{/each}
|
|
456
|
+
</div>
|
|
92
457
|
{/if}
|
|
93
|
-
<div class="edit-panel__spacer"></div>
|
|
94
|
-
<button
|
|
95
|
-
class="edit-panel__btn edit-panel__btn--danger"
|
|
96
|
-
onclick={onremove}
|
|
97
|
-
title="Remove block"
|
|
98
|
-
>
|
|
99
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
100
|
-
<polyline points="3 6 3 14 13 14 13 6" />
|
|
101
|
-
<line x1="1" y1="4" x2="15" y2="4" />
|
|
102
|
-
<line x1="6" y1="2" x2="10" y2="2" />
|
|
103
|
-
<line x1="6" y1="8" x2="6" y2="12" />
|
|
104
|
-
<line x1="10" y1="8" x2="10" y2="12" />
|
|
105
|
-
</svg>
|
|
106
|
-
</button>
|
|
107
|
-
<button
|
|
108
|
-
class="edit-panel__btn"
|
|
109
|
-
onclick={onclose}
|
|
110
|
-
title="Close panel"
|
|
111
|
-
>×</button>
|
|
112
458
|
</div>
|
|
113
459
|
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
460
|
+
{#if block.type === 'rune'}
|
|
461
|
+
{@const rb = block as RuneBlock}
|
|
462
|
+
|
|
463
|
+
<!-- Settings tab -->
|
|
464
|
+
{#if activeTab === 'settings'}
|
|
465
|
+
<div class="edit-panel__tab-panel">
|
|
466
|
+
{#if activeIsContent && activeNode}
|
|
467
|
+
{#if activeNode.type === 'heading'}
|
|
468
|
+
<div class="edit-panel__field-group">
|
|
469
|
+
<label class="edit-panel__field">
|
|
470
|
+
<span class="edit-panel__field-label">Level</span>
|
|
471
|
+
<select
|
|
472
|
+
class="edit-panel__select"
|
|
473
|
+
value={String(activeNode.headingLevel ?? 1)}
|
|
474
|
+
onchange={(e) => handleNestedHeadingLevelChange(Number((e.target as HTMLSelectElement).value))}
|
|
475
|
+
>
|
|
476
|
+
<option value="1">H1</option>
|
|
477
|
+
<option value="2">H2</option>
|
|
478
|
+
<option value="3">H3</option>
|
|
479
|
+
<option value="4">H4</option>
|
|
480
|
+
<option value="5">H5</option>
|
|
481
|
+
<option value="6">H6</option>
|
|
482
|
+
</select>
|
|
483
|
+
</label>
|
|
484
|
+
<label class="edit-panel__field">
|
|
485
|
+
<span class="edit-panel__field-label">Text</span>
|
|
486
|
+
<input
|
|
487
|
+
class="edit-panel__input"
|
|
488
|
+
type="text"
|
|
489
|
+
value={activeNode.headingText ?? ''}
|
|
490
|
+
oninput={(e) => handleNestedHeadingTextChange((e.target as HTMLInputElement).value)}
|
|
491
|
+
/>
|
|
492
|
+
</label>
|
|
493
|
+
</div>
|
|
494
|
+
{:else if activeNode.type === 'fence'}
|
|
495
|
+
<div class="edit-panel__field-group">
|
|
496
|
+
<label class="edit-panel__field">
|
|
497
|
+
<span class="edit-panel__field-label">Language</span>
|
|
498
|
+
<input
|
|
499
|
+
class="edit-panel__input"
|
|
500
|
+
type="text"
|
|
501
|
+
value={activeNode.fenceLanguage ?? ''}
|
|
502
|
+
oninput={(e) => handleNestedFenceLangChange((e.target as HTMLInputElement).value)}
|
|
503
|
+
placeholder="e.g. js, python, html"
|
|
504
|
+
/>
|
|
505
|
+
</label>
|
|
506
|
+
</div>
|
|
507
|
+
{:else}
|
|
508
|
+
<div class="edit-panel__empty-tab">
|
|
509
|
+
<span class="edit-panel__empty-tab-text">No settings for this element</span>
|
|
510
|
+
</div>
|
|
511
|
+
{/if}
|
|
512
|
+
|
|
513
|
+
{:else if activeNode && activeRuneInfo}
|
|
514
|
+
<RuneAttributes
|
|
515
|
+
runeInfo={activeRuneInfo}
|
|
516
|
+
attributes={activeNode.attributes ?? {}}
|
|
517
|
+
onchange={handleNestedAttrsChange}
|
|
140
518
|
/>
|
|
141
|
-
|
|
519
|
+
|
|
520
|
+
{:else if !activeNode}
|
|
521
|
+
{#if runeInfo}
|
|
522
|
+
<RuneAttributes
|
|
523
|
+
{runeInfo}
|
|
524
|
+
attributes={rb.attributes}
|
|
525
|
+
onchange={handleRuneAttrsChange}
|
|
526
|
+
/>
|
|
527
|
+
{:else}
|
|
528
|
+
<div class="edit-panel__unknown">
|
|
529
|
+
<span class="edit-panel__unknown-label">Unknown rune: {rb.runeName}</span>
|
|
530
|
+
{#each Object.entries(rb.attributes) as [key, val]}
|
|
531
|
+
<label class="edit-panel__field">
|
|
532
|
+
<span class="edit-panel__field-label">{key}</span>
|
|
533
|
+
<input
|
|
534
|
+
class="edit-panel__input"
|
|
535
|
+
type="text"
|
|
536
|
+
value={val}
|
|
537
|
+
oninput={(e) => {
|
|
538
|
+
const next = { ...rb.attributes, [key]: (e.target as HTMLInputElement).value };
|
|
539
|
+
handleRuneAttrsChange(next);
|
|
540
|
+
}}
|
|
541
|
+
/>
|
|
542
|
+
</label>
|
|
543
|
+
{/each}
|
|
544
|
+
</div>
|
|
545
|
+
{/if}
|
|
546
|
+
{/if}
|
|
142
547
|
</div>
|
|
548
|
+
{/if}
|
|
143
549
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
550
|
+
<!-- Structure tab -->
|
|
551
|
+
{#if activeTab === 'structure'}
|
|
552
|
+
<div class="edit-panel__tab-panel">
|
|
553
|
+
{#if hasContentModel && resolvedStructure}
|
|
554
|
+
<ContentModelTree
|
|
555
|
+
structure={resolvedStructure}
|
|
556
|
+
rootLabel={activeNode?.runeName ?? rb.runeName}
|
|
557
|
+
onaddfield={handleAddField}
|
|
558
|
+
onremovefield={handleRemoveField}
|
|
559
|
+
onappenditem={handleAppendItem}
|
|
560
|
+
onremovelistitem={handleRemoveListItem}
|
|
561
|
+
oneditfield={handleEditField}
|
|
562
|
+
onfieldselect={handleFieldSelect}
|
|
563
|
+
{selectedField}
|
|
564
|
+
/>
|
|
565
|
+
{:else if !activeNode && hasNestedRunes}
|
|
566
|
+
<ContentTree
|
|
567
|
+
nodes={contentTree}
|
|
568
|
+
{activePath}
|
|
569
|
+
onselect={handleTreeSelect}
|
|
570
|
+
rootLabel={rb.runeName}
|
|
571
|
+
onrootclick={navigateToRoot}
|
|
572
|
+
isRootActive={activePath.length === 0}
|
|
573
|
+
/>
|
|
574
|
+
{/if}
|
|
575
|
+
</div>
|
|
576
|
+
{/if}
|
|
577
|
+
|
|
578
|
+
<!-- Content tab -->
|
|
579
|
+
{#if activeTab === 'content' && !rb.selfClosing}
|
|
580
|
+
<div class="edit-panel__tab-panel">
|
|
581
|
+
{#if activeIsContent && activeNode}
|
|
582
|
+
{#if activeNode.type === 'fence'}
|
|
583
|
+
<div class="edit-panel__content-editor">
|
|
584
|
+
<InlineEditor
|
|
585
|
+
content={activeNode.fenceCode ?? ''}
|
|
586
|
+
onchange={handleNestedFenceCodeChange}
|
|
587
|
+
language={activeNode.fenceLanguage}
|
|
588
|
+
{runes}
|
|
589
|
+
aggregated={() => aggregated}
|
|
590
|
+
/>
|
|
591
|
+
</div>
|
|
592
|
+
{:else if activeNode.type === 'paragraph'}
|
|
593
|
+
<div class="edit-panel__content-editor">
|
|
594
|
+
<InlineEditor
|
|
595
|
+
content={activeNode.source}
|
|
596
|
+
onchange={handleNestedSourceChange}
|
|
597
|
+
{runes}
|
|
598
|
+
aggregated={() => aggregated}
|
|
166
599
|
/>
|
|
167
|
-
</
|
|
168
|
-
{
|
|
600
|
+
</div>
|
|
601
|
+
{:else if activeNode.type === 'heading'}
|
|
602
|
+
<div class="edit-panel__empty-tab">
|
|
603
|
+
<span class="edit-panel__empty-tab-text">Edit heading text in Settings</span>
|
|
604
|
+
</div>
|
|
605
|
+
{:else}
|
|
606
|
+
<div class="edit-panel__field-group">
|
|
607
|
+
<label class="edit-panel__field">
|
|
608
|
+
<span class="edit-panel__field-label">Source</span>
|
|
609
|
+
<textarea
|
|
610
|
+
class="edit-panel__textarea"
|
|
611
|
+
value={activeNode.source}
|
|
612
|
+
oninput={(e) => handleNestedSourceChange((e.target as HTMLTextAreaElement).value)}
|
|
613
|
+
rows={Math.max(4, activeNode.source.split('\n').length)}
|
|
614
|
+
></textarea>
|
|
615
|
+
</label>
|
|
616
|
+
</div>
|
|
617
|
+
{/if}
|
|
618
|
+
|
|
619
|
+
{:else if activeNode && activeRuneInfo}
|
|
620
|
+
{#if !activeNode.selfClosing && activeNode.innerContent !== undefined}
|
|
621
|
+
<div class="edit-panel__content-editor">
|
|
622
|
+
<InlineEditor
|
|
623
|
+
content={activeNode.innerContent}
|
|
624
|
+
onchange={handleNestedContentChange}
|
|
625
|
+
{runes}
|
|
626
|
+
aggregated={() => aggregated}
|
|
627
|
+
/>
|
|
628
|
+
</div>
|
|
629
|
+
{:else}
|
|
630
|
+
<div class="edit-panel__empty-tab">
|
|
631
|
+
<span class="edit-panel__empty-tab-text">This rune has no inner content</span>
|
|
632
|
+
</div>
|
|
633
|
+
{/if}
|
|
634
|
+
|
|
635
|
+
{:else if !activeNode}
|
|
636
|
+
<div class="edit-panel__content-editor">
|
|
637
|
+
<InlineEditor
|
|
638
|
+
content={rb.innerContent}
|
|
639
|
+
onchange={handleRuneContentChange}
|
|
640
|
+
{runes}
|
|
641
|
+
aggregated={() => aggregated}
|
|
642
|
+
/>
|
|
643
|
+
</div>
|
|
644
|
+
{/if}
|
|
645
|
+
</div>
|
|
646
|
+
{/if}
|
|
647
|
+
|
|
648
|
+
{:else}
|
|
649
|
+
<div class="edit-panel__body">
|
|
650
|
+
{#if block.type === 'heading'}
|
|
651
|
+
{@const hb = block as HeadingBlock}
|
|
652
|
+
<div class="edit-panel__field-group">
|
|
653
|
+
<label class="edit-panel__field">
|
|
654
|
+
<span class="edit-panel__field-label">Level</span>
|
|
655
|
+
<select
|
|
656
|
+
class="edit-panel__select"
|
|
657
|
+
value={String(hb.level)}
|
|
658
|
+
onchange={(e) => handleHeadingLevelChange(Number((e.target as HTMLSelectElement).value))}
|
|
659
|
+
>
|
|
660
|
+
<option value="1">H1</option>
|
|
661
|
+
<option value="2">H2</option>
|
|
662
|
+
<option value="3">H3</option>
|
|
663
|
+
<option value="4">H4</option>
|
|
664
|
+
<option value="5">H5</option>
|
|
665
|
+
<option value="6">H6</option>
|
|
666
|
+
</select>
|
|
667
|
+
</label>
|
|
668
|
+
<label class="edit-panel__field">
|
|
669
|
+
<span class="edit-panel__field-label">Text</span>
|
|
670
|
+
<input
|
|
671
|
+
class="edit-panel__input"
|
|
672
|
+
type="text"
|
|
673
|
+
value={hb.text}
|
|
674
|
+
oninput={(e) => handleHeadingTextChange((e.target as HTMLInputElement).value)}
|
|
675
|
+
/>
|
|
676
|
+
</label>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
{:else if block.type === 'fence'}
|
|
680
|
+
{@const fb = block as FenceBlock}
|
|
681
|
+
<div class="edit-panel__field-group">
|
|
682
|
+
<label class="edit-panel__field">
|
|
683
|
+
<span class="edit-panel__field-label">Language</span>
|
|
684
|
+
<input
|
|
685
|
+
class="edit-panel__input"
|
|
686
|
+
type="text"
|
|
687
|
+
value={fb.language}
|
|
688
|
+
oninput={(e) => handleFenceLangChange((e.target as HTMLInputElement).value)}
|
|
689
|
+
placeholder="e.g. js, python, html"
|
|
690
|
+
/>
|
|
691
|
+
</label>
|
|
169
692
|
</div>
|
|
170
|
-
{/if}
|
|
171
|
-
{#if !rb.selfClosing}
|
|
172
693
|
<div class="edit-panel__content-editor">
|
|
173
694
|
<InlineEditor
|
|
174
|
-
content={
|
|
175
|
-
onchange={
|
|
695
|
+
content={fb.code}
|
|
696
|
+
onchange={handleFenceCodeChange}
|
|
697
|
+
language={fb.language}
|
|
176
698
|
{runes}
|
|
177
699
|
aggregated={() => aggregated}
|
|
178
700
|
/>
|
|
179
701
|
</div>
|
|
180
|
-
{/if}
|
|
181
702
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
type="text"
|
|
190
|
-
value={fb.language}
|
|
191
|
-
oninput={(e) => handleFenceLangChange((e.target as HTMLInputElement).value)}
|
|
192
|
-
placeholder="e.g. js, python, html"
|
|
703
|
+
{:else if block.type === 'paragraph'}
|
|
704
|
+
<div class="edit-panel__content-editor">
|
|
705
|
+
<InlineEditor
|
|
706
|
+
content={block.source}
|
|
707
|
+
onchange={handleSourceChange}
|
|
708
|
+
{runes}
|
|
709
|
+
aggregated={() => aggregated}
|
|
193
710
|
/>
|
|
194
|
-
</
|
|
195
|
-
</div>
|
|
196
|
-
<div class="edit-panel__content-editor">
|
|
197
|
-
<InlineEditor
|
|
198
|
-
content={fb.code}
|
|
199
|
-
onchange={handleFenceCodeChange}
|
|
200
|
-
language={fb.language}
|
|
201
|
-
{runes}
|
|
202
|
-
/>
|
|
203
|
-
aggregated={() => aggregated}
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
{:else if block.type === 'paragraph'}
|
|
207
|
-
<div class="edit-panel__content-editor">
|
|
208
|
-
<InlineEditor
|
|
209
|
-
content={block.source}
|
|
210
|
-
onchange={handleSourceChange}
|
|
211
|
-
{runes}
|
|
212
|
-
/>
|
|
213
|
-
aggregated={() => aggregated}
|
|
214
|
-
</div>
|
|
711
|
+
</div>
|
|
215
712
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
713
|
+
{:else}
|
|
714
|
+
<!-- List, quote, image, etc. — raw source editing -->
|
|
715
|
+
<div class="edit-panel__field-group">
|
|
716
|
+
<label class="edit-panel__field">
|
|
717
|
+
<span class="edit-panel__field-label">Source</span>
|
|
718
|
+
<textarea
|
|
719
|
+
class="edit-panel__textarea"
|
|
720
|
+
value={block.source}
|
|
721
|
+
oninput={(e) => handleSourceChange((e.target as HTMLTextAreaElement).value)}
|
|
722
|
+
rows={Math.max(4, block.source.split('\n').length)}
|
|
723
|
+
></textarea>
|
|
724
|
+
</label>
|
|
725
|
+
</div>
|
|
726
|
+
{/if}
|
|
727
|
+
</div>
|
|
728
|
+
{/if}
|
|
231
729
|
</div>
|
|
232
730
|
|
|
233
731
|
<style>
|
|
234
732
|
.edit-panel {
|
|
235
733
|
display: flex;
|
|
236
734
|
flex-direction: column;
|
|
237
|
-
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.edit-panel__top {
|
|
738
|
+
flex-shrink: 0;
|
|
739
|
+
background: var(--ed-surface-0);
|
|
740
|
+
border-bottom: 1px solid var(--ed-border-default);
|
|
238
741
|
}
|
|
239
742
|
|
|
240
743
|
.edit-panel__header {
|
|
241
744
|
display: flex;
|
|
242
745
|
align-items: center;
|
|
243
746
|
gap: 0.5rem;
|
|
244
|
-
padding: var(--ed-space-
|
|
245
|
-
border-bottom: 1px solid var(--ed-border-default);
|
|
246
|
-
background: transparent;
|
|
247
|
-
position: sticky;
|
|
248
|
-
top: 0;
|
|
249
|
-
z-index: 1;
|
|
747
|
+
padding: var(--ed-space-4) var(--ed-space-5);
|
|
250
748
|
}
|
|
251
749
|
|
|
252
750
|
.edit-panel__type {
|
|
@@ -302,10 +800,10 @@
|
|
|
302
800
|
.edit-panel__body {
|
|
303
801
|
flex: 1;
|
|
304
802
|
overflow-y: auto;
|
|
305
|
-
padding: var(--ed-space-
|
|
803
|
+
padding: var(--ed-space-5);
|
|
306
804
|
display: flex;
|
|
307
805
|
flex-direction: column;
|
|
308
|
-
gap: var(--ed-space-
|
|
806
|
+
gap: var(--ed-space-5);
|
|
309
807
|
}
|
|
310
808
|
|
|
311
809
|
.edit-panel__field-group {
|
|
@@ -388,8 +886,8 @@
|
|
|
388
886
|
display: flex;
|
|
389
887
|
flex-direction: column;
|
|
390
888
|
overflow: hidden;
|
|
391
|
-
margin-left: calc(-1 * var(--ed-space-
|
|
392
|
-
margin-right: calc(-1 * var(--ed-space-
|
|
889
|
+
margin-left: calc(-1 * var(--ed-space-5));
|
|
890
|
+
margin-right: calc(-1 * var(--ed-space-5));
|
|
393
891
|
border-top: 1px solid var(--ed-border-subtle);
|
|
394
892
|
}
|
|
395
893
|
|
|
@@ -404,4 +902,65 @@
|
|
|
404
902
|
color: var(--ed-unsaved);
|
|
405
903
|
font-style: italic;
|
|
406
904
|
}
|
|
905
|
+
|
|
906
|
+
/* Tab strip */
|
|
907
|
+
.edit-panel__tabs {
|
|
908
|
+
display: flex;
|
|
909
|
+
gap: 2px;
|
|
910
|
+
background: var(--ed-surface-2);
|
|
911
|
+
border-radius: var(--ed-radius-sm);
|
|
912
|
+
padding: 2px;
|
|
913
|
+
margin: 0 var(--ed-space-4) var(--ed-space-3);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.edit-panel__tab {
|
|
917
|
+
flex: 1;
|
|
918
|
+
display: flex;
|
|
919
|
+
align-items: center;
|
|
920
|
+
justify-content: center;
|
|
921
|
+
gap: 0.35rem;
|
|
922
|
+
padding: 0.35rem 0.5rem;
|
|
923
|
+
border: none;
|
|
924
|
+
background: transparent;
|
|
925
|
+
color: var(--ed-text-muted);
|
|
926
|
+
font-size: var(--ed-text-sm);
|
|
927
|
+
font-weight: 500;
|
|
928
|
+
cursor: pointer;
|
|
929
|
+
border-radius: calc(var(--ed-radius-sm) - 1px);
|
|
930
|
+
transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.edit-panel__tab:hover {
|
|
934
|
+
color: var(--ed-text-secondary);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.edit-panel__tab.active {
|
|
938
|
+
background: var(--ed-surface-0);
|
|
939
|
+
color: var(--ed-text-primary);
|
|
940
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* Tab panels */
|
|
944
|
+
.edit-panel__tab-panel {
|
|
945
|
+
flex: 1;
|
|
946
|
+
overflow-y: auto;
|
|
947
|
+
padding: var(--ed-space-5);
|
|
948
|
+
display: flex;
|
|
949
|
+
flex-direction: column;
|
|
950
|
+
gap: var(--ed-space-5);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/* Empty tab state */
|
|
954
|
+
.edit-panel__empty-tab {
|
|
955
|
+
display: flex;
|
|
956
|
+
align-items: center;
|
|
957
|
+
justify-content: center;
|
|
958
|
+
padding: var(--ed-space-8) var(--ed-space-4);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.edit-panel__empty-tab-text {
|
|
962
|
+
font-size: var(--ed-text-sm);
|
|
963
|
+
color: var(--ed-text-muted);
|
|
964
|
+
font-style: italic;
|
|
965
|
+
}
|
|
407
966
|
</style>
|