autumnnote 1.0.3 → 1.0.5

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.
@@ -11,6 +11,9 @@ 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>`,
16
+ crop: `<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="6 2 6 18 22 18"/><polyline points="2 6 18 6 18 22"/></svg>`,
14
17
  };
15
18
 
16
19
  const SHOW_DELAY = 100;
@@ -40,13 +43,13 @@ export class ImageTooltip {
40
43
  if (img && editable.contains(img) && !img.closest('a[href]')) {
41
44
  this._scheduleShow(img);
42
45
  }
43
- }),
46
+ }, { passive: true }),
44
47
  on(editable, 'mouseout', (e) => {
45
48
  const to = e.relatedTarget;
46
49
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
47
50
  this._scheduleHide();
48
51
  }
49
- }),
52
+ }, { passive: true }),
50
53
  // Hide when image is deselected by clicking elsewhere
51
54
  on(document, 'click', (e) => {
52
55
  if (this._activeImg && !this._activeImg.contains(e.target) && !this._el.contains(e.target)) {
@@ -103,6 +106,16 @@ export class ImageTooltip {
103
106
 
104
107
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
105
108
 
109
+ el.appendChild(this._makeBtn(ICONS.rotateLeft, 'Rotate Left', () => this._rotate(-90)));
110
+ el.appendChild(this._makeBtn(ICONS.rotateRight, 'Rotate Right', () => this._rotate(90)));
111
+
112
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
113
+
114
+ this._cropBtn = this._makeBtn(ICONS.crop, 'Crop Image', () => this._crop());
115
+ el.appendChild(this._cropBtn);
116
+
117
+ el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
118
+
106
119
  this._captionBtn = this._makeBtn(ICONS.caption, 'Add / Edit Caption', () => this._toggleCaption());
107
120
  el.appendChild(this._captionBtn);
108
121
 
@@ -166,7 +179,10 @@ export class ImageTooltip {
166
179
 
167
180
  _show(img) {
168
181
  this._el.style.display = 'flex';
169
- this._positionNear(img);
182
+ // Defer positioning: offsetWidth on a newly-visible element forces layout
183
+ requestAnimationFrame(() => {
184
+ if (this._activeImg) this._positionNear(this._activeImg);
185
+ });
170
186
  }
171
187
 
172
188
  _hide() {
@@ -213,7 +229,7 @@ export class ImageTooltip {
213
229
  target.style.marginRight = value === 'left' ? '12px' : '';
214
230
  this.context.invoke('editor.afterCommand');
215
231
  this.context.invoke('imageResizer.updateOverlay');
216
- this._positionNear(img);
232
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
217
233
  }
218
234
 
219
235
  _setCenter() {
@@ -226,7 +242,7 @@ export class ImageTooltip {
226
242
  target.style.marginRight = 'auto';
227
243
  this.context.invoke('editor.afterCommand');
228
244
  this.context.invoke('imageResizer.updateOverlay');
229
- this._positionNear(img);
245
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
230
246
  }
231
247
 
232
248
  _resetSize() {
@@ -236,7 +252,31 @@ export class ImageTooltip {
236
252
  img.style.height = '';
237
253
  this.context.invoke('editor.afterCommand');
238
254
  this.context.invoke('imageResizer.updateOverlay');
239
- this._positionNear(img);
255
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
256
+ }
257
+
258
+ /**
259
+ * Rotate the active image by `delta` degrees (±90).
260
+ * Reads the existing rotate() value from the transform style so
261
+ * repeated clicks accumulate correctly.
262
+ * @param {number} delta
263
+ */
264
+ _rotate(delta) {
265
+ const img = this._activeImg;
266
+ if (!img) return;
267
+ // Parse current rotation angle from inline transform
268
+ const current = img.style.transform || '';
269
+ const match = current.match(/rotate\((-?[\d.]+)deg\)/);
270
+ const prev = match ? parseFloat(match[1]) : 0;
271
+ const next = (prev + delta + 360) % 360; // normalise to [0, 360)
272
+ // Preserve any other transform functions (e.g. scale), replace only rotate()
273
+ const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
274
+ img.style.transform = cleaned
275
+ ? `${cleaned} rotate(${next}deg)`
276
+ : next === 0 ? '' : `rotate(${next}deg)`;
277
+ this.context.invoke('editor.afterCommand');
278
+ this.context.invoke('imageResizer.updateOverlay');
279
+ requestAnimationFrame(() => { if (this._activeImg) this._positionNear(this._activeImg); });
240
280
  }
241
281
 
242
282
  _delete() {
@@ -253,6 +293,15 @@ export class ImageTooltip {
253
293
  this.context.invoke('editor.afterCommand');
254
294
  }
255
295
 
296
+ _crop() {
297
+ const img = this._activeImg;
298
+ if (!img) return;
299
+ // Hide the tooltip while the crop overlay is active
300
+ this._hide();
301
+ // Delegate to imageCropOverlay module
302
+ this.context.invoke('imageCropOverlay.open', img);
303
+ }
304
+
256
305
  _toggleCaption() {
257
306
  const img = this._activeImg;
258
307
  if (!img) return;
@@ -46,7 +46,7 @@ const SHORTCUTS = [
46
46
  {
47
47
  category: 'Editor',
48
48
  items: [
49
- { keys: 'Shift + ?', action: 'Show this keyboard shortcuts dialog' },
49
+ { keys: 'Ctrl + Shift + /', action: 'Show this keyboard shortcuts dialog' },
50
50
  ],
51
51
  },
52
52
  ];
@@ -110,7 +110,7 @@ export class TableTooltip {
110
110
  if (cell) this._activeCell = cell;
111
111
  this._scheduleShow(table);
112
112
  }
113
- }),
113
+ }, { passive: true }),
114
114
  on(editable, 'mouseout', (e) => {
115
115
  const to = e.relatedTarget;
116
116
  if (!to || (
@@ -120,7 +120,7 @@ export class TableTooltip {
120
120
  )) {
121
121
  this._scheduleHide();
122
122
  }
123
- }),
123
+ }, { passive: true }),
124
124
  on(document, 'click', (e) => {
125
125
  if (this._activeTable &&
126
126
  !this._activeTable.contains(e.target) &&
@@ -155,8 +155,11 @@ export class TableTooltip {
155
155
  let _startW = 0;
156
156
  let _startH = 0;
157
157
  let _colIdx = -1;
158
+ let _colCells = null; // cells cached at drag-start — avoids querySelectorAll every frame
158
159
  let _row = null;
159
160
  let _table = null;
161
+ let _rafDocMove = null; // pending rAF handle for resize drag
162
+ let _rafEditorMove = null; // pending rAF handle for cursor detection
160
163
 
161
164
  const clearHover = () => {
162
165
  if (_nearCell) { _nearCell.style.cursor = ''; _nearCell = null; }
@@ -165,27 +168,29 @@ export class TableTooltip {
165
168
 
166
169
  const onEditorMove = (e) => {
167
170
  if (_resizing) return;
168
- const cell = e.target.closest('td, th');
169
- if (!cell || !editable.contains(cell)) { clearHover(); return; }
170
- if (_nearCell && _nearCell !== cell) {
171
- _nearCell.style.cursor = '';
172
- }
173
- const rect = cell.getBoundingClientRect();
174
- const onRight = Math.abs(e.clientX - rect.right) < HIT;
175
- const onBottom = Math.abs(e.clientY - rect.bottom) < HIT;
176
- if (onRight && onBottom) {
177
- // Corner prefer col-resize
178
- cell.style.cursor = 'col-resize';
179
- _nearCell = cell; _nearEdge = 'col';
180
- } else if (onRight) {
181
- cell.style.cursor = 'col-resize';
182
- _nearCell = cell; _nearEdge = 'col';
183
- } else if (onBottom) {
184
- cell.style.cursor = 'row-resize';
185
- _nearCell = cell; _nearEdge = 'row';
186
- } else {
187
- clearHover();
188
- }
171
+ // Throttle to one check per animation frame — getBoundingClientRect forces reflow
172
+ if (_rafEditorMove !== null) return;
173
+ const target = e.target;
174
+ const clientX = e.clientX;
175
+ const clientY = e.clientY;
176
+ _rafEditorMove = requestAnimationFrame(() => {
177
+ _rafEditorMove = null;
178
+ const cell = target.closest('td, th');
179
+ if (!cell || !editable.contains(cell)) { clearHover(); return; }
180
+ if (_nearCell && _nearCell !== cell) _nearCell.style.cursor = '';
181
+ const rect = cell.getBoundingClientRect();
182
+ const onRight = Math.abs(clientX - rect.right) < HIT;
183
+ const onBottom = Math.abs(clientY - rect.bottom) < HIT;
184
+ if (onRight) {
185
+ cell.style.cursor = 'col-resize';
186
+ _nearCell = cell; _nearEdge = 'col';
187
+ } else if (onBottom) {
188
+ cell.style.cursor = 'row-resize';
189
+ _nearCell = cell; _nearEdge = 'row';
190
+ } else {
191
+ clearHover();
192
+ }
193
+ });
189
194
  };
190
195
 
191
196
  const onEditorDown = (e) => {
@@ -196,8 +201,14 @@ export class TableTooltip {
196
201
  _startY = e.clientY;
197
202
  _table = _nearCell.closest('table');
198
203
  if (_edge === 'col') {
199
- _startW = _nearCell.offsetWidth;
200
- _colIdx = getVisualColIndex(_nearCell);
204
+ _startW = _nearCell.offsetWidth;
205
+ _colIdx = getVisualColIndex(_nearCell);
206
+ // Cache column cells once so onDocMove never runs querySelectorAll per frame
207
+ _colCells = _colIdx >= 0
208
+ ? Array.from(_table.querySelectorAll('tr'))
209
+ .map(r => getCellAtVisualCol(r, _colIdx))
210
+ .filter(Boolean)
211
+ : [];
201
212
  document.body.style.cursor = 'col-resize';
202
213
  } else {
203
214
  _row = _nearCell.closest('tr');
@@ -211,34 +222,42 @@ export class TableTooltip {
211
222
 
212
223
  const onDocMove = (e) => {
213
224
  if (!_resizing) return;
214
- if (_edge === 'col') {
215
- const newW = Math.max(30, _startW + (e.clientX - _startX));
216
- if (_table && _colIdx >= 0) {
217
- Array.from(_table.querySelectorAll('tr')).forEach((r) => {
218
- const c = getCellAtVisualCol(r, _colIdx);
219
- if (c) { c.style.width = `${newW}px`; c.style.minWidth = `${newW}px`; }
220
- });
221
- }
222
- } else {
223
- const newH = Math.max(20, _startH + (e.clientY - _startY));
224
- if (_row) {
225
- Array.from(_row.cells).forEach((c) => {
226
- c.style.height = `${newH}px`;
227
- c.style.minHeight = `${newH}px`;
228
- });
225
+ // Skip if a frame is already scheduled — avoids per-pixel style thrashing
226
+ if (_rafDocMove !== null) return;
227
+ const clientX = e.clientX;
228
+ const clientY = e.clientY;
229
+ _rafDocMove = requestAnimationFrame(() => {
230
+ _rafDocMove = null;
231
+ if (_edge === 'col') {
232
+ const newW = Math.max(30, _startW + (clientX - _startX));
233
+ for (const c of _colCells) {
234
+ c.style.width = `${newW}px`;
235
+ c.style.minWidth = `${newW}px`;
236
+ }
237
+ } else {
238
+ const newH = Math.max(20, _startH + (clientY - _startY));
239
+ if (_row) {
240
+ for (const c of _row.cells) {
241
+ c.style.height = `${newH}px`;
242
+ c.style.minHeight = `${newH}px`;
243
+ }
244
+ }
229
245
  }
230
- }
246
+ });
231
247
  };
232
248
 
233
249
  const onDocUp = () => {
234
250
  if (!_resizing) return;
251
+ // Cancel any in-flight rAF so stale writes don't land after mouseup
252
+ if (_rafDocMove !== null) { cancelAnimationFrame(_rafDocMove); _rafDocMove = null; }
235
253
  _resizing = false;
236
254
  document.body.style.userSelect = '';
237
255
  document.body.style.cursor = '';
238
- _edge = null;
239
- _table = null;
240
- _row = null;
241
- _colIdx = -1;
256
+ _edge = null;
257
+ _table = null;
258
+ _row = null;
259
+ _colIdx = -1;
260
+ _colCells = null;
242
261
  this.context.invoke('editor.afterCommand');
243
262
  };
244
263
 
@@ -375,7 +394,10 @@ export class TableTooltip {
375
394
  _show() {
376
395
  if (!this._activeTable) return;
377
396
  this._el.style.display = 'flex';
378
- this._positionNear(this._activeTable);
397
+ // Defer: offsetWidth on a newly-visible element forces layout synchronously
398
+ requestAnimationFrame(() => {
399
+ if (this._activeTable) this._positionNear(this._activeTable);
400
+ });
379
401
  }
380
402
 
381
403
  _hide() {
@@ -452,7 +474,8 @@ export class TableTooltip {
452
474
  }
453
475
  if (position === 'above') row.parentElement?.insertBefore(newRow, row);
454
476
  else row.insertAdjacentElement('afterend', newRow);
455
- this._positionNear(this._activeTable);
477
+ // Defer positioning until the browser has painted the new layout
478
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
456
479
  this.context.invoke('editor.afterCommand');
457
480
  }
458
481
 
@@ -462,18 +485,16 @@ export class TableTooltip {
462
485
  const table = cell.closest('table');
463
486
  if (!table) return;
464
487
  const visualColIdx = getVisualColIndex(cell);
465
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
466
- const isHeader = r.closest('thead') !== null;
467
- const newCell = createElement(isHeader ? 'th' : 'td', {}, ['\u00a0']);
468
- if (position === 'left') {
469
- const ref = getCellAtVisualCol(r, visualColIdx);
470
- r.insertBefore(newCell, ref);
471
- } else {
472
- const ref = getCellAfterVisualCol(r, visualColIdx);
473
- r.insertBefore(newCell, ref);
474
- }
488
+ const rows = Array.from(table.querySelectorAll('tr'));
489
+ // Batch reads first, then writes — prevents layout thrashing inside the loop
490
+ const refs = rows.map(r => position === 'left'
491
+ ? getCellAtVisualCol(r, visualColIdx)
492
+ : getCellAfterVisualCol(r, visualColIdx));
493
+ const isHeaders = rows.map(r => r.closest('thead') !== null);
494
+ rows.forEach((r, i) => {
495
+ r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
475
496
  });
476
- this._positionNear(this._activeTable);
497
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
477
498
  this.context.invoke('editor.afterCommand');
478
499
  }
479
500
 
@@ -489,7 +510,7 @@ export class TableTooltip {
489
510
  if (bodyRows <= 1 && row.closest('tbody')) return;
490
511
  this._activeCell = null;
491
512
  row.parentElement?.removeChild(row);
492
- this._positionNear(this._activeTable);
513
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
493
514
  this.context.invoke('editor.afterCommand');
494
515
  }
495
516
 
@@ -502,11 +523,11 @@ export class TableTooltip {
502
523
  if (row && row.cells.length <= 1) return;
503
524
  const visualColIdx = getVisualColIndex(cell);
504
525
  this._activeCell = null;
505
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
506
- const c = getCellAtVisualCol(r, visualColIdx);
507
- if (c) r.removeChild(c);
508
- });
509
- this._positionNear(this._activeTable);
526
+ const rows = Array.from(table.querySelectorAll('tr'));
527
+ // Batch reads before writes to avoid forced reflows inside the loop
528
+ const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
529
+ cells.forEach((c, i) => { if (c) rows[i].removeChild(c); });
530
+ requestAnimationFrame(() => this._positionNear(this._activeTable));
510
531
  this.context.invoke('editor.afterCommand');
511
532
  }
512
533
 
@@ -628,15 +649,19 @@ export class TableTooltip {
628
649
 
629
650
  this._sizeApply = (val) => {
630
651
  if (isCol) {
631
- const table = cell.closest('table');
652
+ const table = cell.closest('table');
632
653
  const visualColIdx = getVisualColIndex(cell);
633
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
634
- const c = getCellAtVisualCol(r, visualColIdx);
654
+ const rows = Array.from(table.querySelectorAll('tr'));
655
+ // Batch reads, then writes
656
+ const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
657
+ cells.forEach(c => {
635
658
  if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
636
659
  });
637
660
  } else {
638
661
  const row = cell.closest('tr');
639
- if (row) Array.from(row.cells).forEach((c) => { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; });
662
+ if (row) {
663
+ for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
664
+ }
640
665
  }
641
666
  this.context.invoke('editor.afterCommand');
642
667
  };
@@ -32,6 +32,12 @@ export class VideoResizer {
32
32
 
33
33
  const editable = this.context.layoutInfo.editable;
34
34
 
35
+ let _resizeDebounce = null;
36
+ const onWindowResize = () => {
37
+ clearTimeout(_resizeDebounce);
38
+ _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
39
+ };
40
+
35
41
  this._disposers.push(
36
42
  on(editable, 'click', (e) => this._onEditorClick(e)),
37
43
  on(editable, 'contextmenu', (e) => {
@@ -40,7 +46,7 @@ export class VideoResizer {
40
46
  }),
41
47
  on(document, 'click', (e) => this._onDocClick(e)),
42
48
  on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
43
- on(window, 'resize', () => this._updateOverlayPosition()),
49
+ on(window, 'resize', onWindowResize),
44
50
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
45
51
  );
46
52
 
@@ -192,43 +198,49 @@ export class VideoResizer {
192
198
  const isCorner = pos.length === 2;
193
199
 
194
200
  const editable = this.context.layoutInfo.editable;
201
+ let _raf = null; // rAF handle — at most one write per paint frame
202
+
195
203
  const onMove = (me) => {
196
- const dx = me.clientX - startX;
197
- const dy = me.clientY - startY;
198
- const maxW = editable.clientWidth || Infinity;
199
- let newW = startW;
200
- let newH = startH;
201
-
202
- if (pos.includes('e')) newW = Math.max(80, startW + dx);
203
- if (pos.includes('w')) newW = Math.max(80, startW - dx);
204
- if (pos.includes('s')) newH = Math.max(45, startH + dy);
205
- if (pos.includes('n')) newH = Math.max(45, startH - dy);
206
-
207
- // Clamp to container width
208
- newW = Math.min(newW, maxW);
209
-
210
- if (isCorner) {
211
- if (Math.abs(dx) >= Math.abs(dy)) {
212
- newH = Math.max(45, Math.round(newW / aspectRatio));
213
- } else {
214
- newW = Math.min(Math.max(80, Math.round(newH * aspectRatio)), maxW);
215
- newH = Math.max(45, Math.round(newW / aspectRatio));
204
+ if (_raf !== null) return;
205
+ const clientX = me.clientX;
206
+ const clientY = me.clientY;
207
+ _raf = requestAnimationFrame(() => {
208
+ _raf = null;
209
+ const dx = clientX - startX;
210
+ const dy = clientY - startY;
211
+ const maxW = editable.clientWidth || Infinity;
212
+ let newW = startW;
213
+ let newH = startH;
214
+
215
+ if (pos.includes('e')) newW = Math.max(80, startW + dx);
216
+ if (pos.includes('w')) newW = Math.max(80, startW - dx);
217
+ if (pos.includes('s')) newH = Math.max(45, startH + dy);
218
+ if (pos.includes('n')) newH = Math.max(45, startH - dy);
219
+
220
+ newW = Math.min(newW, maxW);
221
+
222
+ if (isCorner) {
223
+ if (Math.abs(dx) >= Math.abs(dy)) {
224
+ newH = Math.max(45, Math.round(newW / aspectRatio));
225
+ } else {
226
+ newW = Math.min(Math.max(80, Math.round(newH * aspectRatio)), maxW);
227
+ newH = Math.max(45, Math.round(newW / aspectRatio));
228
+ }
229
+ }
230
+
231
+ // Resize wrapper and inner embed via CSS only — attribute writes are redundant
232
+ wrapper.style.width = `${newW}px`;
233
+ wrapper.style.height = `${newH}px`;
234
+ if (embed) {
235
+ embed.style.width = `${newW}px`;
236
+ embed.style.height = `${newH}px`;
216
237
  }
217
- }
218
-
219
- // Resize both the wrapper and the inner embed element
220
- wrapper.style.width = `${newW}px`;
221
- wrapper.style.height = `${newH}px`;
222
- if (embed) {
223
- embed.width = newW;
224
- embed.height = newH;
225
- embed.style.width = `${newW}px`;
226
- embed.style.height = `${newH}px`;
227
- }
228
- this._updateOverlayPosition();
238
+ this._updateOverlayPosition();
239
+ });
229
240
  };
230
241
 
231
242
  const onUp = () => {
243
+ if (_raf !== null) { cancelAnimationFrame(_raf); _raf = null; }
232
244
  document.removeEventListener('mousemove', onMove);
233
245
  document.removeEventListener('mouseup', onUp);
234
246
  this._dragDisposers = null;
@@ -42,13 +42,13 @@ export class VideoTooltip {
42
42
  if (wrapper && editable.contains(wrapper)) {
43
43
  this._scheduleShow(wrapper);
44
44
  }
45
- }),
45
+ }, { passive: true }),
46
46
  on(editable, 'mouseout', (e) => {
47
47
  const to = e.relatedTarget;
48
48
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
49
49
  this._scheduleHide();
50
50
  }
51
- }),
51
+ }, { passive: true }),
52
52
  on(document, 'click', (e) => {
53
53
  if (
54
54
  this._activeWrapper &&
@@ -174,7 +174,10 @@ export class VideoTooltip {
174
174
 
175
175
  _show(wrapper) {
176
176
  this._el.style.display = 'flex';
177
- this._positionNear(wrapper);
177
+ // Defer: offsetWidth on a newly-visible element forces synchronous layout
178
+ requestAnimationFrame(() => {
179
+ if (this._activeWrapper) this._positionNear(this._activeWrapper);
180
+ });
178
181
  }
179
182
 
180
183
  _hide() {
@@ -221,7 +224,7 @@ export class VideoTooltip {
221
224
  wrapper.style.marginRight = value === 'left' ? '12px' : '';
222
225
  this.context.invoke('editor.afterCommand');
223
226
  this.context.invoke('videoResizer.updateOverlay');
224
- this._positionNear(wrapper);
227
+ requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
225
228
  }
226
229
 
227
230
  _setCenter() {
@@ -233,7 +236,7 @@ export class VideoTooltip {
233
236
  wrapper.style.marginRight = 'auto';
234
237
  this.context.invoke('editor.afterCommand');
235
238
  this.context.invoke('videoResizer.updateOverlay');
236
- this._positionNear(wrapper);
239
+ requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
237
240
  }
238
241
 
239
242
  _resetSize() {
@@ -250,7 +253,7 @@ export class VideoTooltip {
250
253
  }
251
254
  this.context.invoke('editor.afterCommand');
252
255
  this.context.invoke('videoResizer.updateOverlay');
253
- this._positionNear(wrapper);
256
+ requestAnimationFrame(() => { if (this._activeWrapper) this._positionNear(this._activeWrapper); });
254
257
  }
255
258
 
256
259
  _delete() {
@@ -969,6 +969,55 @@
969
969
  .an-resize-w { top: calc(50% - 4px); left: -4px; cursor: w-resize; }
970
970
  }
971
971
 
972
+ // ---------------------------------------------------------------------------
973
+ // Image crop overlay
974
+ // ---------------------------------------------------------------------------
975
+
976
+ // The scrim and crop box are appended to <body> and positioned fixed, so they
977
+ // live outside .an-container. All styles are standalone (no SCSS nesting).
978
+
979
+ .an-crop-scrim {
980
+ // Inline styles handle position/z-index/background for performance;
981
+ // clip-path is updated by JS every frame so we only declare the transition.
982
+ transition: clip-path 0s; // disable transition — updated synchronously on drag
983
+ }
984
+
985
+ .an-crop-box {
986
+ // Core layout & interaction via inline styles in JS (position changes every drag frame).
987
+ // Only declare the animated ring so it stays in sync with $an-primary.
988
+ outline: none !important;
989
+
990
+ // Subtle inner shadow that frames the "live" area
991
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45),
992
+ inset 0 0 0 1px rgba(255, 255, 255, 0.25);
993
+ }
994
+
995
+ .an-crop-handle {
996
+ // Inline styles handle size/position; declare hover highlight only.
997
+ &:hover, &:active {
998
+ background: #74affa !important; // $an-primary lightened ~14%
999
+ }
1000
+ }
1001
+
1002
+ .an-crop-toolbar {
1003
+ // Inline styles handle position; add a subtle entrance animation.
1004
+ animation: an-crop-toolbar-in 0.12s ease;
1005
+ }
1006
+
1007
+ @keyframes an-crop-toolbar-in {
1008
+ from { opacity: 0; transform: translateY(4px); }
1009
+ to { opacity: 1; transform: translateY(0); }
1010
+ }
1011
+
1012
+ // Hide canvas-based crop overlay when printing
1013
+ @media print {
1014
+ .an-crop-scrim,
1015
+ .an-crop-box,
1016
+ .an-crop-toolbar {
1017
+ display: none !important;
1018
+ }
1019
+ }
1020
+
972
1021
  // ---------------------------------------------------------------------------
973
1022
  // Video resize overlay
974
1023
  // ---------------------------------------------------------------------------