@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.
Files changed (42) hide show
  1. package/app/dist/assets/{index-D3TQo8gu.js → index-3MvwKRVQ.js} +1 -1
  2. package/app/dist/assets/{index-CeU_s7BB.js → index-B7e694w6.js} +1 -1
  3. package/app/dist/assets/{index-DzHt8ZRh.js → index-BBljOYQu.js} +1 -1
  4. package/app/dist/assets/{index-C72UC2ga.js → index-BEGy_i8o.js} +1 -1
  5. package/app/dist/assets/{index-CqHjo2YT.js → index-BGy7ixjW.js} +1 -1
  6. package/app/dist/assets/{index-DVM3uoxc.js → index-BaLgiiKk.js} +1 -1
  7. package/app/dist/assets/{index-CW02bulk.js → index-BjlNcvOf.js} +1 -1
  8. package/app/dist/assets/{index-DmY6uqAw.js → index-CKfKYVw7.js} +1 -1
  9. package/app/dist/assets/{index-BLuaHLN3.js → index-COFbngzR.js} +1 -1
  10. package/app/dist/assets/{index-BBinZAiy.js → index-CPEo_rvd.js} +1 -1
  11. package/app/dist/assets/{index-D_Y6J00B.js → index-CQDCT-XT.js} +1 -1
  12. package/app/dist/assets/{index-COIPZ34u.js → index-CUmEjEeR.js} +1 -1
  13. package/app/dist/assets/{index-BgCNqcSo.js → index-CeV-Af4N.js} +1 -1
  14. package/app/dist/assets/{index-DW2zI-Ss.js → index-ChbH55h5.js} +1 -1
  15. package/app/dist/assets/index-CzvG5PZT.css +1 -0
  16. package/app/dist/assets/{index-ZLvRNfLb.js → index-D9-aYc3I.js} +1 -1
  17. package/app/dist/assets/{index-BwFn9q4x.js → index-DezxtfNV.js} +1 -1
  18. package/app/dist/assets/{index-CXFMPmtf.js → index-DrI4IfXE.js} +1 -1
  19. package/app/dist/assets/{index-DgIg-QAA.js → index-DwfxgjnU.js} +2 -2
  20. package/app/dist/assets/index-ogrpJNou.js +555 -0
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/api/client.ts +32 -0
  23. package/app/src/lib/components/ActionEditPopover.svelte +41 -19
  24. package/app/src/lib/components/BlockCard.svelte +74 -17
  25. package/app/src/lib/components/BlockEditPanel.svelte +142 -9
  26. package/app/src/lib/components/BlockEditor.svelte +534 -48
  27. package/app/src/lib/components/CodeEditPopover.svelte +281 -63
  28. package/app/src/lib/components/ContentModelTree.svelte +340 -67
  29. package/app/src/lib/components/IconPickerPopover.svelte +389 -0
  30. package/app/src/lib/components/ImageEditPopover.svelte +519 -0
  31. package/app/src/lib/components/InlineEditPopover.svelte +79 -56
  32. package/app/src/lib/components/InlineEditor.svelte +15 -5
  33. package/app/src/lib/components/ProseBlockCard.svelte +446 -0
  34. package/app/src/lib/components/ProseEditPanel.svelte +470 -0
  35. package/app/src/lib/components/RuneAttributes.svelte +51 -0
  36. package/app/src/lib/editor/block-parser.ts +211 -9
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +129 -1
  39. package/dist/server.js.map +1 -1
  40. package/package.json +6 -6
  41. package/app/dist/assets/index-BD2EBUrQ.css +0 -1
  42. package/app/dist/assets/index-BlAOhWAQ.js +0 -453
@@ -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-BlAOhWAQ.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BD2EBUrQ.css">
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">&times;</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
- padding: var(--ed-space-1, 0.25rem) var(--ed-space-2, 0.5rem);
218
- border: 1px solid var(--ed-border-default, #e2e8f0);
219
- border-radius: var(--ed-radius-sm, 4px);
220
- font-size: 11px;
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 100ms, color 100ms;
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
- background: var(--ed-accent, #3b82f6);
228
- border-color: var(--ed-accent, #3b82f6);
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:hover {
233
- opacity: 0.9;
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--remove:hover {
242
- color: var(--ed-text-secondary, #475569);
243
- background: var(--ed-surface-2, #f1f5f9);
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.hidden = false;
150
+ tooltip.classList.add('rf-tooltip-visible');
147
151
  }
148
152
 
149
153
  function hideTooltip() {
150
- tooltip.hidden = true;
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
- if (target !== hoveredEl) {
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 (!tooltip.hidden) {
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
- hoveredEl.classList.remove('rf-editable-hover');
184
- hoveredEl = null;
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
- /* Editable section hover affordance */
304
- [data-name].rf-editable-hover {
305
- outline: 2px dashed rgba(59, 130, 246, 0.5);
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
- cursor: text;
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: 2px dashed rgba(59, 130, 246, 0.3);
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
- applyFieldChange(content => appendListItem(content, resolvedStructure!, fieldName, zoneName));
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
- applyFieldChange(content => removeListItem(content, resolvedStructure!, fieldName, itemIndex, zoneName));
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 handleEditField(fieldName: string, rect: DOMRect, zoneName?: string) {
234
- if (!resolvedStructure || !oneditfield) return;
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 || field.nodes.length !== 1) return;
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));