autumnnote 1.0.1 → 1.0.4

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.
@@ -418,11 +418,23 @@ export class ContextMenu {
418
418
  document.execCommand('hiliteColor', false, fmt.backgroundColor);
419
419
  }
420
420
 
421
+ // Font family — apply BEFORE font-size DOM manipulation so the selection is still intact.
422
+ if (fmt.fontFamily) {
423
+ document.execCommand('fontName', false, fmt.fontFamily);
424
+ }
425
+
421
426
  // Font size — use a unique data-marker to avoid touching pre-existing font[size="7"] nodes.
422
427
  if (fmt.fontSize) {
423
428
  const marker = `fs-${Date.now()}`;
429
+ // Snapshot pre-existing font[size="7"] BEFORE execCommand so we don't accidentally
430
+ // replace nodes that were already in the document (the comment below was aspirational
431
+ // but the original code never actually excluded them — this Set approach is the fix).
432
+ const preExisting = new Set(editable.querySelectorAll('font[size="7"]'));
424
433
  document.execCommand('fontSize', false, '7');
425
- editable.querySelectorAll('font[size="7"]').forEach((el) => el.setAttribute('data-an-tmp', marker));
434
+ // Mark only the NEWLY created font[size="7"] elements.
435
+ editable.querySelectorAll('font[size="7"]').forEach((el) => {
436
+ if (!preExisting.has(el)) el.setAttribute('data-an-tmp', marker);
437
+ });
426
438
  editable.querySelectorAll(`[data-an-tmp="${marker}"]`).forEach((el) => {
427
439
  const span = document.createElement('span');
428
440
  span.style.fontSize = fmt.fontSize;
@@ -432,11 +444,6 @@ export class ContextMenu {
432
444
  });
433
445
  }
434
446
 
435
- // Font family — only apply if it was explicitly set (not just inherited default).
436
- if (fmt.fontFamily) {
437
- document.execCommand('fontName', false, fmt.fontFamily);
438
- }
439
-
440
447
  this.context.invoke('editor.afterCommand');
441
448
  }
442
449
 
@@ -77,12 +77,80 @@ export class Editor {
77
77
  }
78
78
  };
79
79
 
80
+ // Guard: when cursor lands at the <li> element node of a checklist item
81
+ // (before the checkbox), nudge it to the correct text position.
82
+ //
83
+ // mouseup: use caretRangeFromPoint / caretPositionFromPoint so the cursor
84
+ // lands WHERE the user actually clicked (middle, end of text…).
85
+ // keyup : arrow-key navigation may land at <li>[0]; move to start-of-text.
86
+ const fixChecklistCursor = (event) => {
87
+ const sel = window.getSelection();
88
+ if (!sel || !sel.rangeCount) return;
89
+ const r = sel.getRangeAt(0);
90
+ if (!r.collapsed) return;
91
+ const sc = r.startContainer;
92
+
93
+ // Only act when cursor is at the <li> element node itself (not inside
94
+ // a text node — the browser already placed it correctly in that case).
95
+ if (sc.nodeType !== Node.ELEMENT_NODE) return;
96
+ const li = sc.matches && sc.matches('.an-checklist li') ? sc : null;
97
+ if (!li) return;
98
+ const cb = li.querySelector('input[type="checkbox"]');
99
+ if (!cb) return;
100
+
101
+ // For mouse events: ask the browser where the pointer landed so the
102
+ // cursor respects the actual click position inside the text.
103
+ if (event && event.type === 'mouseup') {
104
+ let caret = null;
105
+ if (document.caretRangeFromPoint) {
106
+ caret = document.caretRangeFromPoint(event.clientX, event.clientY);
107
+ } else if (document.caretPositionFromPoint) {
108
+ const cp = document.caretPositionFromPoint(event.clientX, event.clientY);
109
+ if (cp) {
110
+ caret = document.createRange();
111
+ caret.setStart(cp.offsetNode, cp.offset);
112
+ }
113
+ }
114
+ // If the caret from point landed inside a text node of this li, use it
115
+ if (caret && editable.contains(caret.startContainer) &&
116
+ caret.startContainer !== li) {
117
+ caret.collapse(true);
118
+ sel.removeAllRanges();
119
+ sel.addRange(caret);
120
+ return;
121
+ }
122
+ }
123
+
124
+ // Fallback (keyboard nav, or caretRangeFromPoint not available / landed
125
+ // at li again): prefer the first text node after the checkbox so the
126
+ // cursor renders at the padding-left edge (after the visual checkbox)
127
+ // rather than at element-level where the browser may place it at x=0.
128
+ const nr = document.createRange();
129
+ let anchorNode = null;
130
+ for (const child of li.childNodes) {
131
+ if (child !== cb && child.nodeType === Node.TEXT_NODE) {
132
+ anchorNode = child;
133
+ break;
134
+ }
135
+ }
136
+ if (anchorNode) {
137
+ nr.setStart(anchorNode, 0);
138
+ } else {
139
+ nr.setStartAfter(cb);
140
+ }
141
+ nr.collapse(true);
142
+ sel.removeAllRanges();
143
+ sel.addRange(nr);
144
+ };
145
+
80
146
  this._disposers.push(
81
147
  on(editable, 'keydown', onKeydown),
82
148
  on(editable, 'beforeinput', onBeforeInput),
83
149
  on(editable, 'input', onInput),
84
150
  on(document, 'selectionchange', onSelChange),
85
151
  on(editable, 'click', onCheckboxClick),
152
+ on(editable, 'mouseup', fixChecklistCursor),
153
+ on(editable, 'keyup', fixChecklistCursor),
86
154
  );
87
155
  }
88
156
 
@@ -114,8 +182,8 @@ export class Editor {
114
182
  return; // let the native paste event fire
115
183
  }
116
184
 
117
- // Show keyboard shortcuts dialog: Shift+?
118
- if (event.key === '?' && event.shiftKey && !event.ctrlKey && !event.metaKey) {
185
+ // Show keyboard shortcuts dialog: Ctrl+Shift+/
186
+ if (event.key === '/' && event.shiftKey && event.ctrlKey && !event.metaKey) {
119
187
  event.preventDefault();
120
188
  this.context.invoke('shortcutsDialog.show');
121
189
  return;
@@ -318,6 +318,9 @@ export class IconDialog {
318
318
  this._selectedIcon = null;
319
319
  this._activeCat = 'all';
320
320
  this._searchInput.value = '';
321
+ // Clear previously selected icon highlight and disable Insert button
322
+ this._grid.querySelectorAll('.an-icon-cell.active').forEach((c) => c.classList.remove('active'));
323
+ this._insertBtn.setAttribute('disabled', '');
321
324
  this._updateCatTabs();
322
325
  this._filterIcons('', 'all');
323
326
  this._updatePreview(null);
@@ -551,6 +554,7 @@ export class IconDialog {
551
554
  const iconEl = document.createElement('i');
552
555
  iconEl.className = `${cls} fa-${this._selectedIcon}`;
553
556
  iconEl.setAttribute('aria-hidden', 'true');
557
+ iconEl.setAttribute('contenteditable', 'false');
554
558
  if (styleParts.length) iconEl.setAttribute('style', styleParts.join(';'));
555
559
 
556
560
  const savedRange = this._savedRange;
@@ -574,21 +578,26 @@ export class IconDialog {
574
578
  range.deleteContents();
575
579
  range.insertNode(iconEl);
576
580
 
577
- // 4. Insert a zero-width space text node immediately after the icon.
578
- // This is essential when the icon lands at the END of a paragraph:
579
- // browsers cannot place the caret after an inline element that is the
580
- // last child of a block — there is no text node to anchor into.
581
- // The ZWS gives the caret a real text node to sit in, so typing after
582
- // the icon works correctly. It is invisible to the reader.
583
- const zwsNode = document.createTextNode('\u200B');
584
- iconEl.parentNode.insertBefore(zwsNode, iconEl.nextSibling);
585
-
586
- // 5. Place caret inside the ZWS text node (offset 1 = after the ZWS char).
587
- range.setStart(zwsNode, 1);
588
- range.collapse(true);
581
+ // 4. Place cursor after the icon.
582
+ // Keep a text anchor (\u200B) after the icon so ArrowLeft/ArrowRight and
583
+ // caret placement at end-of-line stay stable across browsers.
584
+ let caretTextNode = iconEl.nextSibling;
585
+ if (!caretTextNode || caretTextNode.nodeType !== Node.TEXT_NODE) {
586
+ caretTextNode = document.createTextNode('\u200B');
587
+ iconEl.parentNode.insertBefore(caretTextNode, iconEl.nextSibling);
588
+ } else if (!caretTextNode.textContent) {
589
+ // Split text-node insertions can leave an empty text node after icon.
590
+ // Fill it with ZWS so caret-after-icon remains stable at end-of-line.
591
+ caretTextNode.textContent = '\u200B';
592
+ }
593
+
594
+ const caretOffset = ((caretTextNode.textContent || '').startsWith('\u200B')) ? 1 : 0;
595
+ const caretRange = document.createRange();
596
+ caretRange.setStart(caretTextNode, Math.min(caretOffset, caretTextNode.textContent.length));
597
+ caretRange.collapse(true);
589
598
  if (sel) {
590
599
  sel.removeAllRanges();
591
- sel.addRange(range);
600
+ sel.addRange(caretRange);
592
601
  }
593
602
 
594
603
  // 5. Close dialog last — same as ImageDialog
@@ -596,6 +605,11 @@ export class IconDialog {
596
605
 
597
606
  // 6. Restore editor focus (needed for toolbar refresh via afterCommand)
598
607
  editable.focus();
608
+ if (sel) {
609
+ // Some browsers reset selection when refocusing editable after closing dialog.
610
+ sel.removeAllRanges();
611
+ sel.addRange(caretRange);
612
+ }
599
613
  this.context.invoke('editor.afterCommand');
600
614
  }
601
615
 
@@ -27,14 +27,25 @@ export class ImageResizer {
27
27
  this._activeImg = null;
28
28
  this._overlay = null;
29
29
  this._disposers = [];
30
+ this._positionRaf = null;
30
31
  }
31
32
 
32
33
  initialize() {
33
34
  this._overlay = this._buildOverlay();
34
- document.body.appendChild(this._overlay);
35
+ const container = this.context.layoutInfo.editable.closest('.an-container') || document.body;
36
+ container.appendChild(this._overlay);
37
+ this._container = container;
35
38
 
36
39
  const editable = this.context.layoutInfo.editable;
37
40
 
41
+ // Debounce window resize — _updateOverlayPosition already has rAF gating but
42
+ // every call cancels + re-schedules it; a debounce reduces that churn.
43
+ let _resizeDebounce = null;
44
+ const onWindowResize = () => {
45
+ clearTimeout(_resizeDebounce);
46
+ _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
47
+ };
48
+
38
49
  this._disposers.push(
39
50
  on(editable, 'click', (e) => this._onEditorClick(e)),
40
51
  // Also select on right-click so the highlight shows before the context menu
@@ -44,7 +55,7 @@ export class ImageResizer {
44
55
  }),
45
56
  on(document, 'click', (e) => this._onDocClick(e)),
46
57
  on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
47
- on(window, 'resize', () => this._updateOverlayPosition()),
58
+ on(window, 'resize', onWindowResize),
48
59
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
49
60
  );
50
61
 
@@ -58,6 +69,10 @@ export class ImageResizer {
58
69
  this._dragDisposers.forEach((d) => d());
59
70
  this._dragDisposers = null;
60
71
  }
72
+ if (this._positionRaf) {
73
+ cancelAnimationFrame(this._positionRaf);
74
+ this._positionRaf = null;
75
+ }
61
76
  this._deselect();
62
77
  if (this._overlay && this._overlay.parentNode) {
63
78
  this._overlay.parentNode.removeChild(this._overlay);
@@ -148,10 +163,22 @@ export class ImageResizer {
148
163
  }
149
164
 
150
165
  _updateOverlayPosition() {
166
+ if (this._positionRaf) cancelAnimationFrame(this._positionRaf);
167
+ this._positionRaf = requestAnimationFrame(() => {
168
+ this._positionRaf = null;
169
+ this._updateOverlayPositionNow();
170
+ });
171
+ }
172
+
173
+ _updateOverlayPositionNow() {
151
174
  if (!this._activeImg || !this._overlay) return;
175
+ const offsetParent = this._overlay.offsetParent || this._container;
176
+ const containerRect = offsetParent.getBoundingClientRect();
152
177
  const rect = this._activeImg.getBoundingClientRect();
153
- this._overlay.style.left = `${rect.left}px`;
154
- this._overlay.style.top = `${rect.top}px`;
178
+ const left = rect.left - containerRect.left + offsetParent.scrollLeft;
179
+ const top = rect.top - containerRect.top + offsetParent.scrollTop;
180
+ this._overlay.style.left = `${left}px`;
181
+ this._overlay.style.top = `${top}px`;
155
182
  this._overlay.style.width = `${rect.width}px`;
156
183
  this._overlay.style.height = `${rect.height}px`;
157
184
  }
@@ -168,37 +195,44 @@ export class ImageResizer {
168
195
  const isCorner = pos.length === 2; // 'nw','ne','se','sw'
169
196
 
170
197
  const editable = this.context.layoutInfo.editable;
198
+ let _raf = null; // rAF handle — ensures at most one write per paint frame
199
+
171
200
  const onMove = (me) => {
172
- const dx = me.clientX - startX;
173
- const dy = me.clientY - startY;
174
- const maxW = editable.clientWidth || Infinity;
175
- let newW = startW;
176
- let newH = startH;
177
-
178
- if (pos.includes('e')) newW = Math.max(20, startW + dx);
179
- if (pos.includes('w')) newW = Math.max(20, startW - dx);
180
- if (pos.includes('s')) newH = Math.max(20, startH + dy);
181
- if (pos.includes('n')) newH = Math.max(20, startH - dy);
182
-
183
- // Clamp to container width
184
- newW = Math.min(newW, maxW);
185
-
186
- if (isCorner) {
187
- // Lock aspect ratio: use larger absolute delta to drive both dimensions
188
- if (Math.abs(dx) >= Math.abs(dy)) {
189
- newH = Math.max(20, Math.round(newW / aspectRatio));
190
- } else {
191
- newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
192
- newH = Math.max(20, Math.round(newW / aspectRatio));
201
+ if (_raf !== null) return; // frame already pending, discard this event
202
+ const clientX = me.clientX;
203
+ const clientY = me.clientY;
204
+ _raf = requestAnimationFrame(() => {
205
+ _raf = null;
206
+ const dx = clientX - startX;
207
+ const dy = clientY - startY;
208
+ const maxW = editable.clientWidth || Infinity;
209
+ let newW = startW;
210
+ let newH = startH;
211
+
212
+ if (pos.includes('e')) newW = Math.max(20, startW + dx);
213
+ if (pos.includes('w')) newW = Math.max(20, startW - dx);
214
+ if (pos.includes('s')) newH = Math.max(20, startH + dy);
215
+ if (pos.includes('n')) newH = Math.max(20, startH - dy);
216
+
217
+ newW = Math.min(newW, maxW);
218
+
219
+ if (isCorner) {
220
+ if (Math.abs(dx) >= Math.abs(dy)) {
221
+ newH = Math.max(20, Math.round(newW / aspectRatio));
222
+ } else {
223
+ newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
224
+ newH = Math.max(20, Math.round(newW / aspectRatio));
225
+ }
193
226
  }
194
- }
195
227
 
196
- img.style.width = `${newW}px`;
197
- img.style.height = `${newH}px`;
198
- this._updateOverlayPosition();
228
+ img.style.width = `${newW}px`;
229
+ img.style.height = `${newH}px`;
230
+ this._updateOverlayPosition();
231
+ });
199
232
  };
200
233
 
201
234
  const onUp = () => {
235
+ if (_raf !== null) { cancelAnimationFrame(_raf); _raf = null; }
202
236
  document.removeEventListener('mousemove', onMove);
203
237
  document.removeEventListener('mouseup', onUp);
204
238
  this._dragDisposers = null;
@@ -11,6 +11,8 @@ const ICONS = {
11
11
  originalSize:`<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
12
12
  deleteImg: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
13
13
  caption: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/></svg>`,
14
+ rotateLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><polyline points="3 3 3 8 8 8"/></svg>`,
15
+ rotateRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><polyline points="21 3 21 8 16 8"/></svg>`,
14
16
  };
15
17
 
16
18
  const SHOW_DELAY = 100;
@@ -36,16 +38,17 @@ export class ImageTooltip {
36
38
  this._disposers.push(
37
39
  on(editable, 'mouseover', (e) => {
38
40
  const img = e.target.closest('img');
39
- if (img && editable.contains(img)) {
41
+ // Skip when the image is inside a link — LinkTooltip takes priority there
42
+ if (img && editable.contains(img) && !img.closest('a[href]')) {
40
43
  this._scheduleShow(img);
41
44
  }
42
- }),
45
+ }, { passive: true }),
43
46
  on(editable, 'mouseout', (e) => {
44
47
  const to = e.relatedTarget;
45
48
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
46
49
  this._scheduleHide();
47
50
  }
48
- }),
51
+ }, { passive: true }),
49
52
  // Hide when image is deselected by clicking elsewhere
50
53
  on(document, 'click', (e) => {
51
54
  if (this._activeImg && !this._activeImg.contains(e.target) && !this._el.contains(e.target)) {
@@ -102,6 +105,11 @@ export class ImageTooltip {
102
105
 
103
106
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
104
107
 
108
+ el.appendChild(this._makeBtn(ICONS.rotateLeft, 'Rotate Left', () => this._rotate(-90)));
109
+ el.appendChild(this._makeBtn(ICONS.rotateRight, 'Rotate Right', () => this._rotate(90)));
110
+
111
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
112
+
105
113
  this._captionBtn = this._makeBtn(ICONS.caption, 'Add / Edit Caption', () => this._toggleCaption());
106
114
  el.appendChild(this._captionBtn);
107
115
 
@@ -165,7 +173,10 @@ export class ImageTooltip {
165
173
 
166
174
  _show(img) {
167
175
  this._el.style.display = 'flex';
168
- this._positionNear(img);
176
+ // Defer positioning: offsetWidth on a newly-visible element forces layout
177
+ requestAnimationFrame(() => {
178
+ if (this._activeImg) this._positionNear(this._activeImg);
179
+ });
169
180
  }
170
181
 
171
182
  _hide() {
@@ -205,25 +216,27 @@ export class ImageTooltip {
205
216
  _setFloat(value) {
206
217
  const img = this._activeImg;
207
218
  if (!img) return;
208
- img.style.float = value;
209
- img.style.display = '';
210
- img.style.marginLeft = value === 'right' ? '12px' : '';
211
- img.style.marginRight = value === 'left' ? '12px' : '';
219
+ const target = img.closest('figure.an-figure') || img;
220
+ target.style.float = value;
221
+ target.style.display = '';
222
+ target.style.marginLeft = value === 'right' ? '12px' : '';
223
+ target.style.marginRight = value === 'left' ? '12px' : '';
212
224
  this.context.invoke('editor.afterCommand');
213
225
  this.context.invoke('imageResizer.updateOverlay');
214
- this._positionNear(img);
226
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
215
227
  }
216
228
 
217
229
  _setCenter() {
218
230
  const img = this._activeImg;
219
231
  if (!img) return;
220
- img.style.float = '';
221
- img.style.display = 'block';
222
- img.style.marginLeft = 'auto';
223
- img.style.marginRight = 'auto';
232
+ const target = img.closest('figure.an-figure') || img;
233
+ target.style.float = '';
234
+ target.style.display = 'block';
235
+ target.style.marginLeft = 'auto';
236
+ target.style.marginRight = 'auto';
224
237
  this.context.invoke('editor.afterCommand');
225
238
  this.context.invoke('imageResizer.updateOverlay');
226
- this._positionNear(img);
239
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
227
240
  }
228
241
 
229
242
  _resetSize() {
@@ -233,7 +246,31 @@ export class ImageTooltip {
233
246
  img.style.height = '';
234
247
  this.context.invoke('editor.afterCommand');
235
248
  this.context.invoke('imageResizer.updateOverlay');
236
- this._positionNear(img);
249
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
250
+ }
251
+
252
+ /**
253
+ * Rotate the active image by `delta` degrees (±90).
254
+ * Reads the existing rotate() value from the transform style so
255
+ * repeated clicks accumulate correctly.
256
+ * @param {number} delta
257
+ */
258
+ _rotate(delta) {
259
+ const img = this._activeImg;
260
+ if (!img) return;
261
+ // Parse current rotation angle from inline transform
262
+ const current = img.style.transform || '';
263
+ const match = current.match(/rotate\((-?[\d.]+)deg\)/);
264
+ const prev = match ? parseFloat(match[1]) : 0;
265
+ const next = (prev + delta + 360) % 360; // normalise to [0, 360)
266
+ // Preserve any other transform functions (e.g. scale), replace only rotate()
267
+ const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
268
+ img.style.transform = cleaned
269
+ ? `${cleaned} rotate(${next}deg)`
270
+ : next === 0 ? '' : `rotate(${next}deg)`;
271
+ this.context.invoke('editor.afterCommand');
272
+ this.context.invoke('imageResizer.updateOverlay');
273
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
237
274
  }
238
275
 
239
276
  _delete() {
@@ -241,7 +278,12 @@ export class ImageTooltip {
241
278
  if (!img) return;
242
279
  this._hide();
243
280
  this.context.invoke('imageResizer.deselect');
244
- if (img.parentNode) img.parentNode.removeChild(img);
281
+ const figure = img.closest('figure.an-figure');
282
+ if (figure && figure.parentNode) {
283
+ figure.parentNode.removeChild(figure);
284
+ } else if (img.parentNode) {
285
+ img.parentNode.removeChild(img);
286
+ }
245
287
  this.context.invoke('editor.afterCommand');
246
288
  }
247
289
 
@@ -266,6 +308,23 @@ export class ImageTooltip {
266
308
  // Wrap image in <figure><figcaption>
267
309
  const figure = document.createElement('figure');
268
310
  figure.className = 'an-figure';
311
+
312
+ // Transfer existing alignment from <img> to <figure> (C4 fix)
313
+ if (img.style.float) {
314
+ figure.style.float = img.style.float;
315
+ figure.style.marginLeft = img.style.marginLeft;
316
+ figure.style.marginRight = img.style.marginRight;
317
+ img.style.float = '';
318
+ img.style.marginLeft = '';
319
+ img.style.marginRight = '';
320
+ } else if (img.style.display === 'block' && img.style.marginLeft === 'auto') {
321
+ figure.style.display = 'block';
322
+ figure.style.marginLeft = 'auto';
323
+ figure.style.marginRight = 'auto';
324
+ img.style.display = '';
325
+ img.style.marginLeft = '';
326
+ img.style.marginRight = '';
327
+ }
269
328
  const figcaption = document.createElement('figcaption');
270
329
  figcaption.className = 'an-figcaption';
271
330
  figcaption.textContent = 'Caption';
@@ -281,6 +340,8 @@ export class ImageTooltip {
281
340
  if (sel) { sel.removeAllRanges(); sel.addRange(range); }
282
341
 
283
342
  this.context.invoke('editor.afterCommand');
343
+ // Re-sync the resize overlay: wrapping img in <figure> changes its layout position.
344
+ this.context.invoke('imageResizer.updateOverlay');
284
345
  this._hide();
285
346
  }
286
347
  }
@@ -38,7 +38,8 @@ export class Placeholder {
38
38
 
39
39
  _update() {
40
40
  const editable = this.context.layoutInfo.editable;
41
+ const isFocused = document.activeElement === editable;
41
42
  const isEmpty = !editable.textContent.trim() && !editable.querySelector('img, table, hr');
42
- editable.classList.toggle('an-placeholder', isEmpty);
43
+ editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
43
44
  }
44
45
  }
@@ -130,8 +130,15 @@ export class Statusbar {
130
130
 
131
131
  const applyDelta = (clientY) => {
132
132
  const delta = clientY - startY;
133
- const minH = this.options.minHeight || 100;
134
- containerEl.style.height = `${Math.max(minH, startH + delta)}px`;
133
+ // Compute the true minimum: fixed elements (toolbar + statusbar) must fit
134
+ // inside the container. Sum the offsetHeight of every child that is NOT
135
+ // the editable area, then add a small floor so the editable stays visible.
136
+ const MIN_EDITABLE = 40;
137
+ const fixedH = Array.from(containerEl.children)
138
+ .filter(child => !child.classList.contains('an-editable'))
139
+ .reduce((sum, child) => sum + child.offsetHeight, 0);
140
+ const trueMin = Math.max(this.options.minHeight || 100, fixedH + MIN_EDITABLE);
141
+ containerEl.style.height = `${Math.max(trueMin, startH + delta)}px`;
135
142
  };
136
143
 
137
144
  // Mouse drag
@@ -146,6 +153,11 @@ export class Statusbar {
146
153
  const onMouseDown = (event) => {
147
154
  startY = event.clientY;
148
155
  startH = containerEl.offsetHeight;
156
+ // Clear the editable's inline min-height so the flex layout can compress
157
+ // it freely once the container has a fixed height. Without this, the
158
+ // editable's min-height (set from options.height) overflows the container
159
+ // when the user drags the handle to a size smaller than that value.
160
+ this.context.layoutInfo.editable.style.minHeight = '';
149
161
  document.addEventListener('mousemove', onMouseMove);
150
162
  document.addEventListener('mouseup', onMouseUp);
151
163
  // Track drag-phase listeners so destroy() can remove them mid-drag
@@ -173,6 +185,8 @@ export class Statusbar {
173
185
  if (!touch) return;
174
186
  startY = touch.clientY;
175
187
  startH = containerEl.offsetHeight;
188
+ // Same as onMouseDown: clear editable min-height so flex can compress it
189
+ this.context.layoutInfo.editable.style.minHeight = '';
176
190
  document.addEventListener('touchmove', onTouchMove, { passive: false });
177
191
  document.addEventListener('touchend', onTouchEnd);
178
192
  this._dragDisposers = [