autumnnote 1.0.6 → 1.0.7

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.
@@ -121,9 +121,12 @@ export class ImageDialog {
121
121
  accept: 'image/*',
122
122
  });
123
123
  this._fileInput = fileInput;
124
+ // Hint line shown below the file input for format errors
125
+ const fileHint = createElement('p', { class: 'an-dialog-hint' });
126
+ this._fileHint = fileHint;
124
127
  const d = on(fileInput, 'change', () => this._onFileChange());
125
128
  this._disposers.push(d);
126
- box.append(fileLabel, fileInput);
129
+ box.append(fileLabel, fileInput, fileHint);
127
130
  }
128
131
 
129
132
  // Buttons
@@ -156,10 +159,22 @@ export class ImageDialog {
156
159
  const file = this._fileInput && this._fileInput.files && this._fileInput.files[0];
157
160
  if (!file || !file.type.startsWith('image/')) return;
158
161
 
162
+ // C2: Reject image formats browsers cannot display (TIFF, BMP, etc.).
163
+ const UNSUPPORTED = ['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp'];
164
+ if (UNSUPPORTED.includes(file.type)) {
165
+ const message = `Format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
166
+ if (this._fileHint) this._fileHint.textContent = message;
167
+ this.context.triggerEvent('imageError', { file, message });
168
+ this._fileInput.value = '';
169
+ return;
170
+ }
171
+ if (this._fileHint) this._fileHint.textContent = '';
172
+
159
173
  // Validate file size (max 5 MB by default)
160
174
  const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
161
175
  if (file.size > maxSize) {
162
176
  const message = `Image file is too large. Maximum allowed size is ${this.options.maxImageSize || 5} MB.`;
177
+ if (this._fileHint) this._fileHint.textContent = message;
163
178
  console.warn('[AutumnNote] ImageDialog:', message);
164
179
  this.context.triggerEvent('imageError', { file, message });
165
180
  this._fileInput.value = '';
@@ -168,7 +183,7 @@ export class ImageDialog {
168
183
 
169
184
  const reader = new FileReader();
170
185
  reader.onload = (e) => {
171
- this._urlInput.value = e.target.result;
186
+ this._urlInput.value = /** @type {string} */ (e.target.result);
172
187
  };
173
188
  reader.readAsDataURL(file);
174
189
  }
@@ -50,6 +50,7 @@ export class ImageResizer {
50
50
  on(editable, 'click', (e) => this._onEditorClick(e)),
51
51
  // Also select on right-click so the highlight shows before the context menu
52
52
  on(editable, 'contextmenu', (e) => {
53
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
53
54
  const img = e.target.closest('img');
54
55
  if (img) this._select(img);
55
56
  }),
@@ -127,6 +128,7 @@ export class ImageResizer {
127
128
  }
128
129
 
129
130
  _onEditorClick(e) {
131
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
130
132
  const img = e.target.closest('img');
131
133
  if (img) {
132
134
  // Prevent browser from deselecting the current text selection / opening native resize
@@ -38,6 +38,7 @@ export class ImageTooltip {
38
38
 
39
39
  this._disposers.push(
40
40
  on(editable, 'mouseover', (e) => {
41
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
41
42
  const img = e.target.closest('img');
42
43
  // Skip when the image is inside a link — LinkTooltip takes priority there
43
44
  if (img && editable.contains(img) && !img.closest('a[href]')) {
@@ -227,6 +228,8 @@ export class ImageTooltip {
227
228
  const target = img.closest('figure.an-figure') || img;
228
229
  target.style.float = value;
229
230
  target.style.display = '';
231
+ // Clear explicit width set for centering so the figure is free to float
232
+ if (target !== img) target.style.width = '';
230
233
  target.style.marginLeft = value === 'right' ? '12px' : '';
231
234
  target.style.marginRight = value === 'left' ? '12px' : '';
232
235
  this.context.invoke('editor.afterCommand');
@@ -240,6 +243,9 @@ export class ImageTooltip {
240
243
  const target = img.closest('figure.an-figure') || img;
241
244
  target.style.float = '';
242
245
  target.style.display = 'block';
246
+ // C1/C3: use fit-content so the block figure shrinks to the image width;
247
+ // without an explicit width, margin:auto has no effect on a 100%-wide block.
248
+ if (target !== img) target.style.width = 'fit-content';
243
249
  target.style.marginLeft = 'auto';
244
250
  target.style.marginRight = 'auto';
245
251
  this.context.invoke('editor.afterCommand');
@@ -335,7 +341,10 @@ export class ImageTooltip {
335
341
  img.style.marginLeft = '';
336
342
  img.style.marginRight = '';
337
343
  } else if (img.style.display === 'block' && img.style.marginLeft === 'auto') {
344
+ // C1/C3: use fit-content so the centered block figure tightly wraps the
345
+ // image, making margin:auto actually centre it regardless of image width.
338
346
  figure.style.display = 'block';
347
+ figure.style.width = 'fit-content';
339
348
  figure.style.marginLeft = 'auto';
340
349
  figure.style.marginRight = 'auto';
341
350
  img.style.display = '';
@@ -125,9 +125,13 @@ export class LinkTooltip {
125
125
  }
126
126
 
127
127
  _show(anchor) {
128
+ const isReadOnly = this.context.layoutInfo.container.classList.contains('an-disabled');
128
129
  const url = anchor.getAttribute('href') || '';
129
130
  this._urlLabel.textContent = this._truncateUrl(url);
130
131
  this._urlLabel.title = url;
132
+ // In read-only mode keep only open + copy; hide edit and unlink
133
+ this._editBtn.style.display = isReadOnly ? 'none' : '';
134
+ this._unlinkBtn.style.display = isReadOnly ? 'none' : '';
131
135
  this._el.style.display = 'flex';
132
136
  this._positionNear(anchor);
133
137
  }
@@ -39,7 +39,8 @@ export class Placeholder {
39
39
  _update() {
40
40
  const editable = this.context.layoutInfo.editable;
41
41
  const isFocused = document.activeElement === editable;
42
- const isEmpty = !editable.textContent.trim() && !editable.querySelector('img, table, hr');
42
+ const isEmpty = !editable.textContent.trim() &&
43
+ !editable.querySelector('img, table, hr, .an-video-wrapper');
43
44
  editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
44
45
  }
45
46
  }
@@ -105,6 +105,7 @@ export class TableTooltip {
105
105
 
106
106
  this._disposers.push(
107
107
  on(editable, 'mouseover', (e) => {
108
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
108
109
  const table = e.target.closest('table');
109
110
  if (table && editable.contains(table)) {
110
111
  const cell = e.target.closest('td, th');
@@ -169,6 +170,10 @@ export class TableTooltip {
169
170
 
170
171
  const onEditorMove = (e) => {
171
172
  if (_resizing) return;
173
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) {
174
+ clearHover();
175
+ return;
176
+ }
172
177
  // Throttle to one check per animation frame — getBoundingClientRect forces reflow
173
178
  if (_rafEditorMove !== null) return;
174
179
  const target = e.target;
@@ -195,6 +200,7 @@ export class TableTooltip {
195
200
  };
196
201
 
197
202
  const onEditorDown = (e) => {
203
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
198
204
  if (!_nearCell || !_nearEdge) return;
199
205
  _resizing = true;
200
206
  _edge = _nearEdge;
@@ -513,16 +513,34 @@ export class Toolbar {
513
513
  select.appendChild(opt);
514
514
  });
515
515
 
516
+ // Save the editor selection when the user starts interacting with the
517
+ // dropdown (mousedown fires before the editor loses focus). When the
518
+ // change handler runs, focus has moved to the <select>; we restore the
519
+ // saved range so execCommand / fontSize() act on the intended text.
520
+ /** @type {Range|null} */
521
+ let _savedRange = null;
522
+ const dMousedown = on(select, 'mousedown', () => {
523
+ const sel = window.getSelection();
524
+ _savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
525
+ });
526
+
516
527
  const disposer = on(select, 'change', (e) => {
517
- const value = e.target.value;
518
- const selectedOpt = e.target.options[e.target.selectedIndex];
528
+ const value = /** @type {HTMLSelectElement} */ (e.target).value;
529
+ const selectedOpt = /** @type {HTMLSelectElement} */ (e.target).options[/** @type {HTMLSelectElement} */ (e.target).selectedIndex];
519
530
  if (!value || selectedOpt.disabled) return;
520
531
  this.context.invoke('editor.focus');
532
+ // Restore selection saved on mousedown so the action targets the correct text.
533
+ if (_savedRange) {
534
+ try {
535
+ const sel = window.getSelection();
536
+ if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
537
+ } catch (_) { /* range may be stale if DOM changed */ }
538
+ }
521
539
  def.action(this.context, value);
522
540
  this.context.invoke('editor.afterCommand');
523
541
  });
524
542
 
525
- this._disposers.push(disposer);
543
+ this._disposers.push(dMousedown, disposer);
526
544
  return select;
527
545
  }
528
546
 
@@ -73,12 +73,12 @@ export class VideoDialog {
73
73
  // URL input
74
74
  const urlLabel = createElement('label', { class: 'an-label' });
75
75
  urlLabel.textContent = 'Video URL';
76
- const urlInput = createElement('input', {
76
+ const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
77
77
  type: 'url',
78
78
  class: 'an-input',
79
79
  placeholder: 'YouTube, Vimeo, or direct .mp4 URL',
80
80
  autocomplete: 'off',
81
- });
81
+ }));
82
82
  this._urlInput = urlInput;
83
83
 
84
84
  // Hint (detected source)
@@ -88,14 +88,14 @@ export class VideoDialog {
88
88
  // Width
89
89
  const widthLabel = createElement('label', { class: 'an-label' });
90
90
  widthLabel.textContent = 'Width (px)';
91
- const widthInput = createElement('input', {
91
+ const widthInput = /** @type {HTMLInputElement} */ (createElement('input', {
92
92
  type: 'number',
93
93
  class: 'an-input',
94
94
  placeholder: '560',
95
95
  min: '80',
96
96
  max: '1920',
97
97
  value: '560',
98
- });
98
+ }));
99
99
  this._widthInput = widthInput;
100
100
 
101
101
  // Buttons
@@ -119,7 +119,7 @@ export class VideoDialog {
119
119
  const d1 = on(insertBtn, 'click', () => this._onInsert());
120
120
  const d2 = on(cancelBtn, 'click', () => this._close());
121
121
  const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
122
- const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
122
+ const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
123
123
  this._disposers.push(d0, d1, d2, d3, d4);
124
124
 
125
125
  return overlay;
@@ -41,6 +41,7 @@ export class VideoResizer {
41
41
  this._disposers.push(
42
42
  on(editable, 'click', (e) => this._onEditorClick(e)),
43
43
  on(editable, 'contextmenu', (e) => {
44
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
44
45
  const wrapper = this._findWrapper(e.target);
45
46
  if (wrapper) this._select(wrapper);
46
47
  }),
@@ -48,6 +49,15 @@ export class VideoResizer {
48
49
  on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
49
50
  on(window, 'resize', onWindowResize),
50
51
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
52
+ // D1: Prevent native browser drag of video wrappers. Without this, a user
53
+ // can hold-and-drag to produce a "copy" that lands outside .an-editable,
54
+ // where the .an-video-shield CSS loses its containing context so the
55
+ // copied video becomes directly playable.
56
+ on(editable, 'dragstart', (e) => {
57
+ if (e.target instanceof Element && e.target.closest('.an-video-wrapper')) {
58
+ e.preventDefault();
59
+ }
60
+ }),
51
61
  );
52
62
 
53
63
  return this;
@@ -131,6 +141,7 @@ export class VideoResizer {
131
141
  }
132
142
 
133
143
  _onEditorClick(e) {
144
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
134
145
  const wrapper = this._findWrapper(e.target);
135
146
  if (wrapper) {
136
147
  e.preventDefault();
@@ -37,6 +37,7 @@ export class VideoTooltip {
37
37
 
38
38
  this._disposers.push(
39
39
  on(editable, 'mouseover', (e) => {
40
+ if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
40
41
  // The shield div sits on top of iframes — we detect hover via it or the wrapper
41
42
  const wrapper = e.target.closest('.an-video-wrapper');
42
43
  if (wrapper && editable.contains(wrapper)) {
@@ -78,6 +78,9 @@ export function renderLayout(targetEl, options) {
78
78
  // Read-only mode
79
79
  if (options.readOnly) {
80
80
  container.classList.add('an-disabled');
81
+ editable.querySelectorAll('ul.an-checklist input[type="checkbox"]').forEach((cb) => {
82
+ cb.setAttribute('disabled', '');
83
+ });
81
84
  }
82
85
 
83
86
  // Text direction
@@ -50,6 +50,20 @@
50
50
  user-select: text;
51
51
  // Subtle visual cue that the area is read-only.
52
52
  background: #fafafa;
53
+
54
+ // Block checklist checkbox interaction in read-only mode.
55
+ ul.an-checklist input[type='checkbox'] {
56
+ pointer-events: none;
57
+ cursor: default;
58
+ }
59
+
60
+ // Prevent images and videos from being dragged out.
61
+ img,
62
+ .an-video-wrapper {
63
+ -webkit-user-drag: none;
64
+ user-drag: none;
65
+ pointer-events: none;
66
+ }
53
67
  }
54
68
  }
55
69
 
@@ -87,6 +101,12 @@
87
101
  align-items: center;
88
102
  gap: 2px;
89
103
 
104
+ // Icon-only toolbar buttons should not have horizontal padding so all
105
+ // buttons appear consistently square (min-width = height = $an-btn-height).
106
+ // Dialog buttons (.an-dialog-actions .an-btn) keep their padding via the
107
+ // lower-specificity .an-btn rule.
108
+ > .an-btn { padding: 0; }
109
+
90
110
  // Separator between groups
91
111
  & + .an-btn-group {
92
112
  position: relative;
@@ -388,6 +408,7 @@
388
408
  // =============================================================================
389
409
 
390
410
  .an-editable {
411
+ position: relative; // Establishes containing block for the ::before placeholder
391
412
  flex: 1;
392
413
  padding: $an-editable-pad;
393
414
  min-height: 0;
@@ -397,18 +418,18 @@
397
418
  overflow-y: auto;
398
419
  word-break: break-word;
399
420
 
400
- // Placeholder — only shown when not focused.
401
- // float:left is the standard contenteditable placeholder technique (used by
402
- // Quill.js): the pseudo-element is out of normal flow so it never shifts the
403
- // cursor, yet it renders visually at the start of the empty editing area.
404
- // Avoid position:absolute here — without position:relative on the parent it
405
- // would be positioned relative to .an-container (contain:layout) and overlap
406
- // the toolbar.
421
+ // Placeholder — shown only when the editable is empty and not focused.
422
+ // Uses position:absolute so it never shifts the cursor or block content,
423
+ // and requires position:relative on this element (added above) so it is
424
+ // scoped to the editable area and never overlaps the toolbar.
407
425
  &.an-placeholder:not(:focus)::before {
408
426
  content: attr(data-placeholder);
409
427
  color: $an-muted;
410
428
  pointer-events: none;
411
- float: left;
429
+ position: absolute;
430
+ top: 12px; // matches top padding from $an-editable-pad (12px 14px)
431
+ left: 14px; // matches left padding
432
+ right: 14px;
412
433
  }
413
434
 
414
435
  // Content styles
@@ -472,9 +493,21 @@
472
493
  inset: 0;
473
494
  z-index: 2;
474
495
  cursor: default;
496
+ pointer-events: all; // Explicitly block pointer events reaching the iframe
475
497
  }
476
498
  }
477
499
 
500
+ // Ensure video shield stays effective even when a video copy moves outside
501
+ // .an-editable temporarily (e.g. native drag-and-drop).
502
+ // This global rule mirrors the scoped one above.
503
+ @at-root .an-video-wrapper .an-video-shield {
504
+ position: absolute;
505
+ inset: 0;
506
+ z-index: 2;
507
+ cursor: default;
508
+ pointer-events: all;
509
+ }
510
+
478
511
  // Table styles — target both classed and unclassed tables
479
512
  table, .an-table {
480
513
  border-collapse: collapse;
@@ -934,6 +967,91 @@
934
967
  background: $an-border;
935
968
  }
936
969
 
970
+ // Color-strip variant — SVG stacked above a thin color bar (matches toolbar)
971
+ .an-context-icon--color {
972
+ flex-direction: column;
973
+ gap: 2px;
974
+ width: 16px;
975
+ align-items: center;
976
+
977
+ .an-context-icon-svg {
978
+ display: flex;
979
+ align-items: center;
980
+ justify-content: center;
981
+ line-height: 0;
982
+ }
983
+ }
984
+
985
+ .an-context-color-strip {
986
+ display: block;
987
+ width: 100%;
988
+ height: 3px;
989
+ border-radius: 1px;
990
+ flex-shrink: 0;
991
+ }
992
+
993
+ // Color palette inside context menu
994
+ .an-context-color-palette {
995
+ display: flex;
996
+ flex-wrap: wrap;
997
+ gap: 3px;
998
+ padding: 6px 12px 2px;
999
+ }
1000
+
1001
+ .an-context-color-swatch {
1002
+ width: 15px;
1003
+ height: 15px;
1004
+ border-radius: 3px;
1005
+ border: 1px solid rgba(0, 0, 0, 0.12);
1006
+ cursor: pointer;
1007
+ flex-shrink: 0;
1008
+ position: relative;
1009
+ transition: transform 0.1s;
1010
+
1011
+ &:hover {
1012
+ transform: scale(1.28);
1013
+ z-index: 1;
1014
+ border-color: rgba(0, 0, 0, 0.4);
1015
+ }
1016
+ }
1017
+
1018
+ .an-context-color-none {
1019
+ display: flex;
1020
+ align-items: center;
1021
+ justify-content: center;
1022
+ background: #fff !important;
1023
+ color: #999;
1024
+
1025
+ svg { display: block; }
1026
+
1027
+ &:hover { color: #333; }
1028
+ }
1029
+
1030
+ .an-context-color-custom {
1031
+ display: flex;
1032
+ align-items: center;
1033
+ gap: 8px;
1034
+ padding: 4px 12px 8px;
1035
+ font-size: 12px;
1036
+ color: $an-muted;
1037
+ cursor: pointer;
1038
+
1039
+ input[type='color'] {
1040
+ width: 18px;
1041
+ height: 18px;
1042
+ padding: 0;
1043
+ border: 1px solid $an-border;
1044
+ border-radius: 3px;
1045
+ cursor: pointer;
1046
+ background: transparent;
1047
+ -webkit-appearance: none;
1048
+ appearance: none;
1049
+ }
1050
+
1051
+ span { flex: 1; }
1052
+ &:hover span { color: $an-text; }
1053
+ }
1054
+
937
1055
  // ---------------------------------------------------------------------------
938
1056
  // Image resize overlay
939
1057
  // ---------------------------------------------------------------------------
@@ -1378,7 +1496,11 @@ $an-video-accent: #8b5cf6; // violet-500
1378
1496
  margin: 0.75em 0;
1379
1497
  text-align: center;
1380
1498
 
1381
- img { display: block; max-width: 100%; }
1499
+ img {
1500
+ display: block;
1501
+ max-width: 100%;
1502
+ margin: 0 auto; // Centers img when figure uses fit-content or explicit width
1503
+ }
1382
1504
  }
1383
1505
 
1384
1506
  figcaption.an-figcaption {
@@ -1650,6 +1772,21 @@ $an-video-accent: #8b5cf6; // violet-500
1650
1772
  .an-context-item { color: #cdd6f4; }
1651
1773
  .an-context-sep { background: #3f3f5f; }
1652
1774
 
1775
+ .an-context-color-swatch {
1776
+ border-color: rgba(255, 255, 255, 0.15);
1777
+ &:hover { border-color: rgba(255, 255, 255, 0.5); }
1778
+ }
1779
+
1780
+ .an-context-color-none {
1781
+ background: #1e1e2e !important;
1782
+ color: #888;
1783
+ &:hover { color: #ccc; }
1784
+ }
1785
+
1786
+ .an-context-color-custom {
1787
+ input[type='color'] { border-color: #3f3f5f; }
1788
+ }
1789
+
1653
1790
  .an-icon-cat {
1654
1791
  border-color: #3f3f5f;
1655
1792
  color: #a6adc8;