@refrakt-md/editor 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/dist/assets/{index-D3TQo8gu.js → index-3MvwKRVQ.js} +1 -1
- package/app/dist/assets/{index-CeU_s7BB.js → index-B7e694w6.js} +1 -1
- package/app/dist/assets/{index-DzHt8ZRh.js → index-BBljOYQu.js} +1 -1
- package/app/dist/assets/{index-C72UC2ga.js → index-BEGy_i8o.js} +1 -1
- package/app/dist/assets/{index-CqHjo2YT.js → index-BGy7ixjW.js} +1 -1
- package/app/dist/assets/{index-DVM3uoxc.js → index-BaLgiiKk.js} +1 -1
- package/app/dist/assets/{index-CW02bulk.js → index-BjlNcvOf.js} +1 -1
- package/app/dist/assets/{index-DmY6uqAw.js → index-CKfKYVw7.js} +1 -1
- package/app/dist/assets/{index-BLuaHLN3.js → index-COFbngzR.js} +1 -1
- package/app/dist/assets/{index-BBinZAiy.js → index-CPEo_rvd.js} +1 -1
- package/app/dist/assets/{index-D_Y6J00B.js → index-CQDCT-XT.js} +1 -1
- package/app/dist/assets/{index-COIPZ34u.js → index-CUmEjEeR.js} +1 -1
- package/app/dist/assets/{index-BgCNqcSo.js → index-CeV-Af4N.js} +1 -1
- package/app/dist/assets/{index-DW2zI-Ss.js → index-ChbH55h5.js} +1 -1
- package/app/dist/assets/index-CzvG5PZT.css +1 -0
- package/app/dist/assets/{index-ZLvRNfLb.js → index-D9-aYc3I.js} +1 -1
- package/app/dist/assets/{index-BwFn9q4x.js → index-DezxtfNV.js} +1 -1
- package/app/dist/assets/{index-CXFMPmtf.js → index-DrI4IfXE.js} +1 -1
- package/app/dist/assets/{index-DgIg-QAA.js → index-DwfxgjnU.js} +2 -2
- package/app/dist/assets/index-ogrpJNou.js +555 -0
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +32 -0
- package/app/src/lib/components/ActionEditPopover.svelte +41 -19
- package/app/src/lib/components/BlockCard.svelte +74 -17
- package/app/src/lib/components/BlockEditPanel.svelte +142 -9
- package/app/src/lib/components/BlockEditor.svelte +534 -48
- package/app/src/lib/components/CodeEditPopover.svelte +281 -63
- package/app/src/lib/components/ContentModelTree.svelte +340 -67
- 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 +79 -56
- 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/components/RuneAttributes.svelte +51 -0
- package/app/src/lib/editor/block-parser.ts +211 -9
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +129 -1
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/app/dist/assets/index-BD2EBUrQ.css +0 -1
- package/app/dist/assets/index-BlAOhWAQ.js +0 -453
package/app/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>refrakt editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-ogrpJNou.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CzvG5PZT.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
|
@@ -192,12 +192,44 @@ export async function fetchPagesList(): Promise<PageListItem[]> {
|
|
|
192
192
|
return data.pages;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// ── Asset management ────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export interface ImageAsset {
|
|
198
|
+
path: string;
|
|
199
|
+
name: string;
|
|
200
|
+
size: number;
|
|
201
|
+
modified: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function fetchAssets(): Promise<ImageAsset[]> {
|
|
205
|
+
const res = await fetch(`${BASE}/api/assets`);
|
|
206
|
+
if (!res.ok) throw new Error(`Failed to load assets: ${res.status}`);
|
|
207
|
+
const data = await res.json();
|
|
208
|
+
return data.images;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function uploadAsset(file: File): Promise<{ path: string; name: string }> {
|
|
212
|
+
const formData = new FormData();
|
|
213
|
+
formData.append('file', file);
|
|
214
|
+
|
|
215
|
+
const res = await fetch(`${BASE}/api/assets/upload`, {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: formData,
|
|
218
|
+
});
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
const data = await res.json().catch(() => ({}));
|
|
221
|
+
throw new Error((data as { error?: string }).error ?? `Upload failed: ${res.status}`);
|
|
222
|
+
}
|
|
223
|
+
return res.json();
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
// ── Rune metadata ───────────────────────────────────────────────────────
|
|
196
227
|
|
|
197
228
|
export interface RuneAttributeInfo {
|
|
198
229
|
type: string;
|
|
199
230
|
required: boolean;
|
|
200
231
|
values?: string[];
|
|
232
|
+
description?: string;
|
|
201
233
|
}
|
|
202
234
|
|
|
203
235
|
// ── Serialized content model types (from server) ────────────────────────
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
>
|
|
97
97
|
<div class="action-edit-popover__header">
|
|
98
98
|
<span class="action-edit-popover__label">action</span>
|
|
99
|
+
<button class="action-edit-popover__close" onclick={onclose} aria-label="Close">×</button>
|
|
99
100
|
</div>
|
|
100
101
|
|
|
101
102
|
<div class="action-edit-popover__row">
|
|
@@ -157,6 +158,9 @@
|
|
|
157
158
|
/* ── Header ──────────────────────────────────────────── */
|
|
158
159
|
|
|
159
160
|
.action-edit-popover__header {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: space-between;
|
|
160
164
|
padding: 0 var(--ed-space-1, 0.25rem);
|
|
161
165
|
}
|
|
162
166
|
|
|
@@ -168,6 +172,20 @@
|
|
|
168
172
|
letter-spacing: 0.04em;
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
.action-edit-popover__close {
|
|
176
|
+
background: none;
|
|
177
|
+
border: none;
|
|
178
|
+
font-size: 18px;
|
|
179
|
+
line-height: 1;
|
|
180
|
+
color: var(--ed-text-muted, #94a3b8);
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
padding: 0 4px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.action-edit-popover__close:hover {
|
|
186
|
+
color: var(--ed-text-primary, #1a1a2e);
|
|
187
|
+
}
|
|
188
|
+
|
|
171
189
|
/* ── Rows ────────────────────────────────────────────── */
|
|
172
190
|
|
|
173
191
|
.action-edit-popover__row {
|
|
@@ -214,32 +232,36 @@
|
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
.action-edit-popover__btn {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
gap: var(--ed-space-2);
|
|
239
|
+
height: 28px;
|
|
240
|
+
padding: 0 var(--ed-space-3);
|
|
241
|
+
border: 1px solid var(--ed-text-secondary);
|
|
242
|
+
border-radius: var(--ed-radius-sm);
|
|
243
|
+
background: transparent;
|
|
244
|
+
color: var(--ed-text-secondary);
|
|
245
|
+
font-size: var(--ed-text-sm);
|
|
221
246
|
font-weight: 500;
|
|
222
247
|
cursor: pointer;
|
|
223
|
-
transition: background
|
|
248
|
+
transition: background var(--ed-transition-fast), color var(--ed-transition-fast), border-color var(--ed-transition-fast);
|
|
249
|
+
white-space: nowrap;
|
|
224
250
|
}
|
|
225
251
|
|
|
226
|
-
.action-edit-popover__btn--apply {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
color: white;
|
|
252
|
+
.action-edit-popover__btn:hover:not(.action-edit-popover__btn--apply):not(:disabled) {
|
|
253
|
+
border-color: var(--ed-text-primary);
|
|
254
|
+
color: var(--ed-text-primary);
|
|
230
255
|
}
|
|
231
256
|
|
|
232
|
-
.action-edit-popover__btn--apply
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.action-edit-popover__btn--remove {
|
|
237
|
-
background: transparent;
|
|
238
|
-
color: var(--ed-text-muted, #94a3b8);
|
|
257
|
+
.action-edit-popover__btn--apply {
|
|
258
|
+
background: var(--ed-text-primary);
|
|
259
|
+
color: #ffffff;
|
|
260
|
+
border-color: var(--ed-text-primary);
|
|
239
261
|
}
|
|
240
262
|
|
|
241
|
-
.action-edit-popover__btn--
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
.action-edit-popover__btn--apply:hover:not(:disabled) {
|
|
264
|
+
background: var(--ed-text-secondary);
|
|
265
|
+
border-color: var(--ed-text-secondary);
|
|
244
266
|
}
|
|
245
267
|
</style>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { ThemeConfig, RendererNode } from '@refrakt-md/transform';
|
|
4
4
|
import { renderBlockPreview } from '../preview/block-renderer.js';
|
|
5
5
|
import { initRuneBehaviors } from '@refrakt-md/behaviors';
|
|
6
|
-
export type EditType = 'inline' | 'link' | 'code';
|
|
6
|
+
export type EditType = 'inline' | 'link' | 'code' | 'image' | 'icon';
|
|
7
7
|
|
|
8
8
|
export interface SectionClickInfo {
|
|
9
9
|
dataName: string;
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
editType: EditType;
|
|
13
13
|
/** For link-type edits: the href from the <a> child */
|
|
14
14
|
href?: string;
|
|
15
|
+
/** For icon-type edits: the current icon name from data-icon attribute */
|
|
16
|
+
iconName?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
type InteractiveTarget =
|
|
@@ -133,6 +135,8 @@
|
|
|
133
135
|
/** Attach click and hover handlers to interactive elements within the wrapper */
|
|
134
136
|
function attachSectionHandlers(wrapper: HTMLElement, runeConfigMap: Map<string, import('@refrakt-md/transform').RuneConfig>) {
|
|
135
137
|
let hoveredEl: HTMLElement | null = null;
|
|
138
|
+
let removeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
139
|
+
const HOVER_DEBOUNCE = 75; // ms — bridges list-item gaps
|
|
136
140
|
|
|
137
141
|
// Tooltip element — lives inside the shadow DOM wrapper
|
|
138
142
|
const tooltip = document.createElement('div');
|
|
@@ -143,23 +147,49 @@
|
|
|
143
147
|
tooltip.textContent = label;
|
|
144
148
|
tooltip.style.left = `${me.clientX + 12}px`;
|
|
145
149
|
tooltip.style.top = `${me.clientY + 12}px`;
|
|
146
|
-
tooltip.
|
|
150
|
+
tooltip.classList.add('rf-tooltip-visible');
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
function hideTooltip() {
|
|
150
|
-
tooltip.
|
|
154
|
+
tooltip.classList.remove('rf-tooltip-visible');
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
hideTooltip();
|
|
154
|
-
|
|
155
157
|
wrapper.addEventListener('mouseover', (e) => {
|
|
156
158
|
const result = findInteractiveTarget(e.target as HTMLElement, wrapper, runeConfigMap);
|
|
157
159
|
const target = result?.el ?? null;
|
|
158
|
-
|
|
160
|
+
|
|
161
|
+
// Cancel any pending deferred removal/switch
|
|
162
|
+
if (removeTimer !== null) {
|
|
163
|
+
clearTimeout(removeTimer);
|
|
164
|
+
removeTimer = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (target === hoveredEl) {
|
|
168
|
+
// Re-entered the same target — already highlighted
|
|
169
|
+
} else if (target && hoveredEl && hoveredEl.contains(target)) {
|
|
170
|
+
// Moving to a child of current target — switch immediately
|
|
171
|
+
hoveredEl.classList.remove('rf-editable-hover');
|
|
172
|
+
hoveredEl = target;
|
|
173
|
+
hoveredEl.classList.add('rf-editable-hover');
|
|
174
|
+
} else if (hoveredEl && target && target.contains(hoveredEl)) {
|
|
175
|
+
// Moving to an ancestor (gap-crossing case) — defer the switch
|
|
176
|
+
const oldHovered = hoveredEl;
|
|
177
|
+
removeTimer = setTimeout(() => {
|
|
178
|
+
if (hoveredEl === oldHovered) {
|
|
179
|
+
oldHovered.classList.remove('rf-editable-hover');
|
|
180
|
+
hoveredEl = target;
|
|
181
|
+
hoveredEl.classList.add('rf-editable-hover');
|
|
182
|
+
}
|
|
183
|
+
removeTimer = null;
|
|
184
|
+
}, HOVER_DEBOUNCE);
|
|
185
|
+
return; // Keep current tooltip during deferred switch
|
|
186
|
+
} else {
|
|
187
|
+
// Completely different target — switch immediately
|
|
159
188
|
hoveredEl?.classList.remove('rf-editable-hover');
|
|
160
189
|
hoveredEl = target;
|
|
161
190
|
hoveredEl?.classList.add('rf-editable-hover');
|
|
162
191
|
}
|
|
192
|
+
|
|
163
193
|
if (result) {
|
|
164
194
|
const label = result.type === 'section'
|
|
165
195
|
? result.dataName
|
|
@@ -171,7 +201,7 @@
|
|
|
171
201
|
});
|
|
172
202
|
|
|
173
203
|
wrapper.addEventListener('mousemove', (e) => {
|
|
174
|
-
if (
|
|
204
|
+
if (tooltip.classList.contains('rf-tooltip-visible')) {
|
|
175
205
|
tooltip.style.left = `${e.clientX + 12}px`;
|
|
176
206
|
tooltip.style.top = `${e.clientY + 12}px`;
|
|
177
207
|
}
|
|
@@ -180,8 +210,15 @@
|
|
|
180
210
|
wrapper.addEventListener('mouseout', (e) => {
|
|
181
211
|
const related = (e as MouseEvent).relatedTarget as HTMLElement | null;
|
|
182
212
|
if (hoveredEl && (!related || !hoveredEl.contains(related))) {
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
if (removeTimer !== null) {
|
|
214
|
+
clearTimeout(removeTimer);
|
|
215
|
+
}
|
|
216
|
+
const el = hoveredEl;
|
|
217
|
+
removeTimer = setTimeout(() => {
|
|
218
|
+
el.classList.remove('rf-editable-hover');
|
|
219
|
+
if (hoveredEl === el) hoveredEl = null;
|
|
220
|
+
removeTimer = null;
|
|
221
|
+
}, HOVER_DEBOUNCE);
|
|
185
222
|
hideTooltip();
|
|
186
223
|
}
|
|
187
224
|
});
|
|
@@ -199,15 +236,22 @@
|
|
|
199
236
|
const rect = el.getBoundingClientRect();
|
|
200
237
|
// For link edits, extract the href from the <a> child
|
|
201
238
|
const anchor = editType === 'link' ? el.querySelector('a') as HTMLAnchorElement | null : null;
|
|
239
|
+
// For image edits, extract the src from the <img> child
|
|
240
|
+
const imgEl = editType === 'image' ? el.querySelector('img') as HTMLImageElement | null : null;
|
|
202
241
|
// For code edits, extract text from the <code> element to avoid
|
|
203
242
|
// picking up structural text (language labels, copy buttons, etc.)
|
|
204
243
|
const codeEl = editType === 'code' ? el.querySelector('code') : null;
|
|
244
|
+
// For icon edits, extract the icon name from the data-icon attribute
|
|
245
|
+
const iconEl = editType === 'icon'
|
|
246
|
+
? (el.hasAttribute('data-icon') ? el : el.querySelector('[data-icon]')) as HTMLElement | null
|
|
247
|
+
: null;
|
|
205
248
|
onsectionclick?.({
|
|
206
249
|
dataName,
|
|
207
250
|
text: (codeEl ?? el).textContent?.trim() ?? '',
|
|
208
251
|
rect,
|
|
209
252
|
editType,
|
|
210
|
-
href: anchor?.getAttribute('href') ?? undefined,
|
|
253
|
+
href: anchor?.getAttribute('href') ?? imgEl?.getAttribute('src') ?? undefined,
|
|
254
|
+
iconName: iconEl?.getAttribute('data-icon') ?? undefined,
|
|
211
255
|
});
|
|
212
256
|
} else {
|
|
213
257
|
// type === 'rune' → open block edit panel
|
|
@@ -300,12 +344,22 @@ ${hlCss}
|
|
|
300
344
|
grid-column: full;
|
|
301
345
|
padding-inline: max(var(--rf-content-gutter, 1.5rem), calc((100% - var(--rf-content-max)) / 2));
|
|
302
346
|
}
|
|
303
|
-
/*
|
|
304
|
-
[data-name]
|
|
305
|
-
outline: 2px dashed
|
|
347
|
+
/* Base: invisible outline + transition (persists on remove for fade-out) */
|
|
348
|
+
[data-name] {
|
|
349
|
+
outline: 2px dashed transparent;
|
|
350
|
+
outline-offset: 4px;
|
|
351
|
+
border-radius: 4px;
|
|
352
|
+
transition: outline-color 150ms ease;
|
|
353
|
+
}
|
|
354
|
+
[data-rune] {
|
|
355
|
+
outline: 2px dashed transparent;
|
|
306
356
|
outline-offset: 4px;
|
|
307
357
|
border-radius: 4px;
|
|
308
|
-
|
|
358
|
+
transition: outline-color 150ms ease;
|
|
359
|
+
}
|
|
360
|
+
/* Editable section hover affordance */
|
|
361
|
+
[data-name].rf-editable-hover {
|
|
362
|
+
outline-color: rgba(59, 130, 246, 0.5);
|
|
309
363
|
}
|
|
310
364
|
[data-name].rf-editable-hover,
|
|
311
365
|
[data-name].rf-editable-hover * {
|
|
@@ -313,9 +367,7 @@ ${hlCss}
|
|
|
313
367
|
}
|
|
314
368
|
/* Rune root hover affordance — opens block edit panel */
|
|
315
369
|
[data-rune].rf-editable-hover {
|
|
316
|
-
outline:
|
|
317
|
-
outline-offset: 4px;
|
|
318
|
-
border-radius: 4px;
|
|
370
|
+
outline-color: rgba(59, 130, 246, 0.3);
|
|
319
371
|
}
|
|
320
372
|
[data-rune].rf-editable-hover,
|
|
321
373
|
[data-rune].rf-editable-hover * {
|
|
@@ -335,6 +387,11 @@ ${hlCss}
|
|
|
335
387
|
pointer-events: none;
|
|
336
388
|
z-index: 10000;
|
|
337
389
|
white-space: nowrap;
|
|
390
|
+
opacity: 0;
|
|
391
|
+
transition: opacity 120ms ease;
|
|
392
|
+
}
|
|
393
|
+
.rf-edit-tooltip.rf-tooltip-visible {
|
|
394
|
+
opacity: 1;
|
|
338
395
|
}
|
|
339
396
|
</style>
|
|
340
397
|
<div class="rf-preview-wrapper">${html}</div>`;
|
|
@@ -19,9 +19,14 @@
|
|
|
19
19
|
removeFieldContent,
|
|
20
20
|
appendListItem,
|
|
21
21
|
removeListItem,
|
|
22
|
+
reorderListItem,
|
|
23
|
+
appendGreedyItem,
|
|
24
|
+
removeGreedyItem,
|
|
25
|
+
reorderGreedyItem,
|
|
26
|
+
splitListItems,
|
|
22
27
|
} from '../editor/block-parser.js';
|
|
23
|
-
import { resolveContentStructure } from '../editor/content-model-resolver.js';
|
|
24
|
-
import type { SectionMapping } from '../editor/section-mapper.js';
|
|
28
|
+
import { resolveContentStructure, type ResolvedField } from '../editor/content-model-resolver.js';
|
|
29
|
+
import type { SectionMapping, CommandMapping } from '../editor/section-mapper.js';
|
|
25
30
|
import { stripInlineMarkdown } from '../editor/inline-markdown.js';
|
|
26
31
|
import RuneAttributes from './RuneAttributes.svelte';
|
|
27
32
|
import ContentTree from './ContentTree.svelte';
|
|
@@ -39,9 +44,10 @@
|
|
|
39
44
|
onremove: () => void;
|
|
40
45
|
onclose: () => void;
|
|
41
46
|
oneditfield?: (dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) => void;
|
|
47
|
+
oneditcode?: (code: string, language: string, rect: DOMRect, mapping: CommandMapping) => void;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
let { block, runeMap, runes, aggregated = {}, initialRuneIndex = null, onupdate, onremove, onclose, oneditfield }: Props = $props();
|
|
50
|
+
let { block, runeMap, runes, aggregated = {}, initialRuneIndex = null, onupdate, onremove, onclose, oneditfield, oneditcode }: Props = $props();
|
|
45
51
|
|
|
46
52
|
let label = $derived(blockLabel(block));
|
|
47
53
|
|
|
@@ -220,18 +226,54 @@
|
|
|
220
226
|
applyFieldChange(content => removeFieldContent(content, resolvedStructure!, fieldName, zoneName));
|
|
221
227
|
}
|
|
222
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;
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isGreedyItemField(field: ResolvedField): boolean {
|
|
242
|
+
return field.greedy && field.match !== 'any';
|
|
243
|
+
}
|
|
244
|
+
|
|
223
245
|
function handleAppendItem(fieldName: string, zoneName?: string) {
|
|
224
246
|
if (!resolvedStructure) return;
|
|
225
|
-
|
|
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));
|
|
252
|
+
}
|
|
226
253
|
}
|
|
227
254
|
|
|
228
255
|
function handleRemoveListItem(fieldName: string, itemIndex: number, zoneName?: string) {
|
|
229
256
|
if (!resolvedStructure) return;
|
|
230
|
-
|
|
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
|
+
}
|
|
231
263
|
}
|
|
232
264
|
|
|
233
|
-
function
|
|
234
|
-
if (!resolvedStructure
|
|
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;
|
|
235
277
|
// Find the field in the resolved structure
|
|
236
278
|
let field;
|
|
237
279
|
if (resolvedStructure.type === 'sequence') {
|
|
@@ -240,9 +282,30 @@
|
|
|
240
282
|
const zone = resolvedStructure.zones.find(z => z.name === zoneName);
|
|
241
283
|
field = zone?.fields.find(f => f.name === fieldName);
|
|
242
284
|
}
|
|
243
|
-
if (!field || !field.filled
|
|
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;
|
|
244
308
|
|
|
245
|
-
const source = field.nodes[0].source;
|
|
246
309
|
const trimmed = source.trim();
|
|
247
310
|
|
|
248
311
|
// Strip markdown prefix (heading markers, blockquote markers)
|
|
@@ -270,6 +333,70 @@
|
|
|
270
333
|
oneditfield(fieldName, inlineContent, rect, mapping);
|
|
271
334
|
}
|
|
272
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
|
+
|
|
273
400
|
// ── Edit handlers ────────────────────────────────────────────
|
|
274
401
|
|
|
275
402
|
function handleHeadingTextChange(text: string) {
|
|
@@ -558,7 +685,10 @@
|
|
|
558
685
|
onremovefield={handleRemoveField}
|
|
559
686
|
onappenditem={handleAppendItem}
|
|
560
687
|
onremovelistitem={handleRemoveListItem}
|
|
688
|
+
onreorderlistitem={handleReorderListItem}
|
|
561
689
|
oneditfield={handleEditField}
|
|
690
|
+
oneditlistitem={handleEditListItem}
|
|
691
|
+
onnavigaterune={handleNavigateRune}
|
|
562
692
|
onfieldselect={handleFieldSelect}
|
|
563
693
|
{selectedField}
|
|
564
694
|
/>
|
|
@@ -732,6 +862,8 @@
|
|
|
732
862
|
.edit-panel {
|
|
733
863
|
display: flex;
|
|
734
864
|
flex-direction: column;
|
|
865
|
+
flex: 1;
|
|
866
|
+
min-height: 0;
|
|
735
867
|
}
|
|
736
868
|
|
|
737
869
|
.edit-panel__top {
|
|
@@ -885,6 +1017,7 @@
|
|
|
885
1017
|
.edit-panel__content-editor {
|
|
886
1018
|
display: flex;
|
|
887
1019
|
flex-direction: column;
|
|
1020
|
+
flex-shrink: 0;
|
|
888
1021
|
overflow: hidden;
|
|
889
1022
|
margin-left: calc(-1 * var(--ed-space-5));
|
|
890
1023
|
margin-right: calc(-1 * var(--ed-space-5));
|