autumnnote 1.0.8 → 1.1.0

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 (41) hide show
  1. package/README.md +401 -628
  2. package/dist/autumnnote.css +169 -0
  3. package/dist/autumnnote.es.js +3669 -179
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +3668 -178
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +3 -2
  8. package/src/js/Context.js +18 -2
  9. package/src/js/core/func.js +5 -5
  10. package/src/js/i18n/de.js +320 -0
  11. package/src/js/i18n/en.js +328 -0
  12. package/src/js/i18n/es.js +320 -0
  13. package/src/js/i18n/fr.js +321 -0
  14. package/src/js/i18n/index.js +59 -0
  15. package/src/js/i18n/ja.js +321 -0
  16. package/src/js/i18n/ko.js +320 -0
  17. package/src/js/i18n/vi.js +321 -0
  18. package/src/js/i18n/zh.js +321 -0
  19. package/src/js/index.js +2 -1
  20. package/src/js/module/AutoSaveRestore.js +126 -0
  21. package/src/js/module/BubbleToolbar.js +243 -0
  22. package/src/js/module/CodeTooltip.js +12 -9
  23. package/src/js/module/ContextMenu.js +41 -37
  24. package/src/js/module/EmojiDialog.js +8 -7
  25. package/src/js/module/FindReplace.js +14 -13
  26. package/src/js/module/IconDialog.js +14 -13
  27. package/src/js/module/ImageDialog.js +17 -16
  28. package/src/js/module/ImageTooltip.js +13 -12
  29. package/src/js/module/LinkDialog.js +10 -9
  30. package/src/js/module/LinkTooltip.js +6 -5
  31. package/src/js/module/MarkdownShortcuts.js +253 -0
  32. package/src/js/module/Mention.js +337 -0
  33. package/src/js/module/ShortcutsDialog.js +5 -4
  34. package/src/js/module/Statusbar.js +6 -5
  35. package/src/js/module/TableTooltip.js +22 -19
  36. package/src/js/module/Toolbar.js +21 -15
  37. package/src/js/module/VideoDialog.js +10 -9
  38. package/src/js/module/VideoTooltip.js +12 -11
  39. package/src/js/settings.js +23 -0
  40. package/src/styles/autumnnote.scss +191 -0
  41. package/types/index.d.ts +114 -1
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Mention.js — @mention autocomplete support.
3
+ *
4
+ * Activated when `mention.onSearch` option is provided.
5
+ *
6
+ * When the user types the trigger character (default `@`) followed by at least
7
+ * `mention.minChars` characters, `onSearch(query, callback)` is called.
8
+ * The callback receives an array of `{ id, label, avatar? }` items which are
9
+ * rendered in a floating dropdown. Selecting an item inserts a non-editable
10
+ * mention chip and fires `onInsert` (if provided) to override the default HTML.
11
+ *
12
+ * Options shape (passed as `mention` option object):
13
+ * {
14
+ * trigger: '@',
15
+ * minChars: 1,
16
+ * maxResults: 8,
17
+ * debounce: 200,
18
+ * onSearch: (query, callback) => void,
19
+ * onInsert: (item) => string | null,
20
+ * mentionClass: 'an-mention',
21
+ * allowSpaces: false,
22
+ * }
23
+ */
24
+
25
+ import { on } from '../core/dom.js';
26
+
27
+ export class Mention {
28
+ /** @param {import('../Context.js').Context} context */
29
+ constructor(context) {
30
+ this.context = context;
31
+
32
+ /** @type {HTMLElement|null} */
33
+ this._dropdown = null;
34
+ /** @type {string} */
35
+ this._query = '';
36
+ /** @type {number|null} */
37
+ this._debounceTimer = null;
38
+ /** @type {number} current highlighted index */
39
+ this._activeIndex = -1;
40
+ /** @type {Array<{id, label, avatar?}>} */
41
+ this._items = [];
42
+ /** @type {boolean} */
43
+ this._open = false;
44
+ /** Position of trigger character in the text node */
45
+ this._triggerNode = null;
46
+ this._triggerOffset = 0;
47
+ /** @type {DOMRect|null} Caret rect captured synchronously during input event */
48
+ this._caretRect = null;
49
+
50
+ this._disposers = [];
51
+ }
52
+
53
+ initialize() {
54
+ const cfg = this.context.options.mention;
55
+ if (!cfg || typeof cfg.onSearch !== 'function') return this;
56
+ this._cfg = {
57
+ trigger: cfg.trigger || '@',
58
+ minChars: cfg.minChars ?? 0,
59
+ maxResults: cfg.maxResults ?? 8,
60
+ debounce: cfg.debounce ?? 200,
61
+ onSearch: cfg.onSearch,
62
+ onInsert: cfg.onInsert || null,
63
+ mentionClass: cfg.mentionClass || 'an-mention',
64
+ allowSpaces: cfg.allowSpaces || false,
65
+ };
66
+
67
+ this._buildDropdown();
68
+
69
+ const editable = this.context.layoutInfo.editable;
70
+ const d1 = on(editable, 'keydown', (e) => this._onKeydown(e));
71
+ const d2 = on(editable, 'input', () => this._onInput());
72
+ const d3 = on(document, 'click', (e) => this._onDocClick(e));
73
+ this._disposers.push(d1, d2, d3);
74
+ return this;
75
+ }
76
+
77
+ destroy() {
78
+ clearTimeout(this._debounceTimer);
79
+ if (this._dropdown && this._dropdown.parentNode) {
80
+ this._dropdown.parentNode.removeChild(this._dropdown);
81
+ }
82
+ this._dropdown = null;
83
+ this._disposers.forEach((d) => d());
84
+ this._disposers = [];
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Dropdown DOM
89
+ // ---------------------------------------------------------------------------
90
+
91
+ _buildDropdown() {
92
+ const el = document.createElement('div');
93
+ el.className = 'an-mention-dropdown';
94
+ el.setAttribute('role', 'listbox');
95
+ document.body.appendChild(el);
96
+ this._dropdown = el;
97
+ }
98
+
99
+ _renderItems(items) {
100
+ const dd = this._dropdown;
101
+ dd.innerHTML = '';
102
+ this._items = items.slice(0, this._cfg.maxResults);
103
+ this._activeIndex = this._items.length > 0 ? 0 : -1;
104
+
105
+ this._items.forEach((item, i) => {
106
+ const li = document.createElement('div');
107
+ li.className = 'an-mention-item';
108
+ li.setAttribute('role', 'option');
109
+ li.dataset.index = i;
110
+ if (item.avatar) {
111
+ const img = document.createElement('img');
112
+ img.src = item.avatar;
113
+ img.className = 'an-mention-avatar';
114
+ img.alt = '';
115
+ li.appendChild(img);
116
+ }
117
+ const label = document.createElement('span');
118
+ label.textContent = item.label;
119
+ li.appendChild(label);
120
+ li.addEventListener('mousedown', (e) => e.preventDefault());
121
+ li.addEventListener('click', () => this._select(i));
122
+ dd.appendChild(li);
123
+ });
124
+
125
+ this._highlightItem(this._activeIndex);
126
+ }
127
+
128
+ _highlightItem(index) {
129
+ if (!this._dropdown) return;
130
+ this._dropdown.querySelectorAll('.an-mention-item').forEach((el, i) => {
131
+ el.classList.toggle('an-mention-active', i === index);
132
+ });
133
+ this._activeIndex = index;
134
+ }
135
+
136
+ _positionDropdown() {
137
+ const dd = this._dropdown;
138
+ const rect = this._caretRect;
139
+ if (!rect || rect.height === 0) return;
140
+
141
+ // Measure while invisible to avoid layout flash
142
+ dd.style.visibility = 'hidden';
143
+ dd.style.display = 'block';
144
+
145
+ const ddh = dd.offsetHeight;
146
+ const ddw = dd.offsetWidth;
147
+
148
+ // position:fixed — coords are already viewport-relative, no scroll offset needed
149
+ let top = rect.bottom + 4;
150
+ let left = rect.left;
151
+
152
+ if (rect.bottom + ddh + 8 > window.innerHeight) {
153
+ top = rect.top - ddh - 4;
154
+ }
155
+ left = Math.max(8, Math.min(left, window.innerWidth - ddw - 8));
156
+
157
+ dd.style.top = `${top}px`;
158
+ dd.style.left = `${left}px`;
159
+ dd.style.visibility = '';
160
+ }
161
+
162
+ _showDropdown() {
163
+ this._open = true;
164
+ this._positionDropdown();
165
+ }
166
+
167
+ _hideDropdown() {
168
+ if (this._dropdown) this._dropdown.style.display = 'none';
169
+ this._open = false;
170
+ this._items = [];
171
+ this._activeIndex = -1;
172
+ this._triggerNode = null;
173
+ this._caretRect = null;
174
+ this._query = '';
175
+ }
176
+
177
+ /**
178
+ * Captures the caret rect synchronously during the input event.
179
+ * Must be called while the DOM event is still live — getClientRects() on a
180
+ * collapsed range is reliable at this point but often empty inside async callbacks.
181
+ */
182
+ _captureCaretRect() {
183
+ // Prefer a range over the trigger character — it has non-zero width and
184
+ // getBoundingClientRect() reliably returns a valid rect.
185
+ if (this._triggerNode && this._triggerNode.isConnected) {
186
+ try {
187
+ const r = document.createRange();
188
+ const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
189
+ r.setStart(this._triggerNode, this._triggerOffset);
190
+ r.setEnd(this._triggerNode, end);
191
+ const candidate = r.getBoundingClientRect();
192
+ if (candidate.height > 0) {
193
+ this._caretRect = candidate;
194
+ return;
195
+ }
196
+ } catch (_) {}
197
+ }
198
+
199
+ // Fallback: use getClientRects() on the current collapsed selection
200
+ const sel = window.getSelection();
201
+ if (!sel || !sel.rangeCount) return;
202
+ const rects = sel.getRangeAt(0).getClientRects();
203
+ if (rects.length > 0) {
204
+ this._caretRect = rects[rects.length - 1];
205
+ }
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Query detection
210
+ // ---------------------------------------------------------------------------
211
+
212
+ _getQueryAtCursor() {
213
+ const sel = window.getSelection();
214
+ if (!sel || !sel.rangeCount) return null;
215
+ const range = sel.getRangeAt(0);
216
+ if (!range.collapsed) return null;
217
+
218
+ const node = range.startContainer;
219
+ if (node.nodeType !== Node.TEXT_NODE) return null;
220
+
221
+ const text = node.textContent.slice(0, range.startOffset);
222
+ const trigger = this._cfg.trigger;
223
+
224
+ // Find the last occurrence of trigger in the text before cursor
225
+ const triggerIdx = text.lastIndexOf(trigger);
226
+ if (triggerIdx === -1) return null;
227
+
228
+ const afterTrigger = text.slice(triggerIdx + trigger.length);
229
+
230
+ // No spaces allowed unless configured
231
+ if (!this._cfg.allowSpaces && /\s/.test(afterTrigger)) return null;
232
+
233
+ if (afterTrigger.length < this._cfg.minChars) return null;
234
+
235
+ this._triggerNode = node;
236
+ this._triggerOffset = triggerIdx;
237
+ return afterTrigger;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Events
242
+ // ---------------------------------------------------------------------------
243
+
244
+ _onInput() {
245
+ if (!this._cfg) return;
246
+ const query = this._getQueryAtCursor();
247
+ if (query === null) {
248
+ this._hideDropdown();
249
+ return;
250
+ }
251
+
252
+ // Capture caret rect NOW, synchronously while the input event is live.
253
+ // Inside a setTimeout/debounce callback the selection may still be valid
254
+ // but getClientRects() on a collapsed range often returns empty in that context.
255
+ this._captureCaretRect();
256
+
257
+ this._query = query;
258
+ clearTimeout(this._debounceTimer);
259
+ this._debounceTimer = setTimeout(() => {
260
+ this._cfg.onSearch(this._query, (items) => {
261
+ if (!Array.isArray(items) || items.length === 0) {
262
+ this._hideDropdown();
263
+ return;
264
+ }
265
+ this._renderItems(items);
266
+ this._showDropdown();
267
+ });
268
+ }, this._cfg.debounce);
269
+ }
270
+
271
+ _onKeydown(e) {
272
+ if (!this._cfg || !this._open) return;
273
+
274
+ if (e.key === 'ArrowDown') {
275
+ e.preventDefault();
276
+ const next = (this._activeIndex + 1) % this._items.length;
277
+ this._highlightItem(next);
278
+ } else if (e.key === 'ArrowUp') {
279
+ e.preventDefault();
280
+ const prev = (this._activeIndex - 1 + this._items.length) % this._items.length;
281
+ this._highlightItem(prev);
282
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
283
+ if (this._activeIndex >= 0) {
284
+ e.preventDefault();
285
+ this._select(this._activeIndex);
286
+ }
287
+ } else if (e.key === 'Escape') {
288
+ this._hideDropdown();
289
+ }
290
+ }
291
+
292
+ _onDocClick(e) {
293
+ if (!this._open) return;
294
+ if (this._dropdown && this._dropdown.contains(e.target)) return;
295
+ this._hideDropdown();
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Insert
300
+ // ---------------------------------------------------------------------------
301
+
302
+ _select(index) {
303
+ const item = this._items[index];
304
+ if (!item) return;
305
+
306
+ // Delete the trigger + query text from the DOM
307
+ if (this._triggerNode) {
308
+ const node = this._triggerNode;
309
+ const before = node.textContent.slice(0, this._triggerOffset);
310
+ const after = node.textContent.slice(this._triggerOffset + this._cfg.trigger.length + this._query.length);
311
+ node.textContent = before + after;
312
+
313
+ // Move cursor to where the trigger was
314
+ const sel = window.getSelection();
315
+ const range = document.createRange();
316
+ range.setStart(node, this._triggerOffset);
317
+ range.collapse(true);
318
+ sel.removeAllRanges();
319
+ sel.addRange(range);
320
+ }
321
+
322
+ // Build the chip HTML
323
+ let html;
324
+ if (typeof this._cfg.onInsert === 'function') {
325
+ html = this._cfg.onInsert(item);
326
+ }
327
+ if (!html) {
328
+ const cls = this._cfg.mentionClass;
329
+ html = `<span class="${cls}" data-mention-id="${item.id}" contenteditable="false">@${item.label}</span>`;
330
+ }
331
+
332
+ // Insert a trailing space so the cursor lands outside the chip
333
+ this.context.invoke('editor.insertHTML', html + '&#8203;');
334
+ this._hideDropdown();
335
+ this.context.triggerEvent('change', this.context.getHTML());
336
+ }
337
+ }
@@ -93,7 +93,7 @@ export class ShortcutsDialog {
93
93
  class: 'an-dialog-overlay',
94
94
  role: 'dialog',
95
95
  'aria-modal': 'true',
96
- 'aria-label': 'Keyboard Shortcuts',
96
+ 'aria-label': this.context.locale.shortcutsDialog.ariaLabel,
97
97
  });
98
98
 
99
99
  const box = createElement('div', { class: 'an-dialog-box an-shortcuts-box' });
@@ -101,18 +101,19 @@ export class ShortcutsDialog {
101
101
  // Title row with close button
102
102
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
103
103
  const title = createElement('h3', { class: 'an-dialog-title' });
104
- title.textContent = 'Keyboard Shortcuts';
104
+ title.textContent = this.context.locale.shortcutsDialog.title;
105
105
  const closeBtn = createElement('button', {
106
106
  type: 'button',
107
107
  class: 'an-icon-close',
108
- 'aria-label': 'Close',
108
+ 'aria-label': this.context.locale.shortcutsDialog.close,
109
109
  });
110
110
  closeBtn.textContent = '×';
111
111
  this._closeBtn = closeBtn;
112
112
  titleRow.append(title, closeBtn);
113
113
  box.appendChild(titleRow);
114
114
 
115
- SHORTCUTS.forEach(({ category, items }) => {
115
+ const shortcuts = this.context.locale.shortcutsDialog.shortcuts || SHORTCUTS;
116
+ shortcuts.forEach(({ category, items }) => {
116
117
  const catEl = createElement('div', { class: 'an-shortcuts-cat' });
117
118
  catEl.textContent = category;
118
119
  box.appendChild(catEl);
@@ -82,7 +82,7 @@ export class Statusbar {
82
82
  if (this.options.resizable !== false) {
83
83
  const handle = createElement('div', {
84
84
  class: 'an-resize-handle',
85
- title: 'Resize editor',
85
+ title: this.context.locale.statusbar.resizeHandle,
86
86
  'aria-hidden': 'true',
87
87
  });
88
88
  this._bindResize(handle);
@@ -220,12 +220,13 @@ export class Statusbar {
220
220
  const maxWords = this.options.maxWords || 0;
221
221
  const maxChars = this.options.maxChars || 0;
222
222
 
223
+ const LS = this.context.locale.statusbar;
223
224
  this._wordCountEl.textContent = maxWords
224
- ? `Words: ${words}/${maxWords}`
225
- : `Words: ${words}`;
225
+ ? LS.wordsLimit(words, maxWords)
226
+ : LS.words(words);
226
227
  this._charCountEl.textContent = maxChars
227
- ? `Chars: ${chars}/${maxChars}`
228
- : `Chars: ${chars}`;
228
+ ? LS.charsLimit(chars, maxChars)
229
+ : LS.chars(chars);
229
230
 
230
231
  // Apply warning / exceeded styles
231
232
  _applyLimitClass(this._wordCountEl, words, maxWords);
@@ -376,55 +376,56 @@ export class TableTooltip {
376
376
  // ---------------------------------------------------------------------------
377
377
 
378
378
  _buildTooltip() {
379
+ const L = this.context.locale.tooltips.table;
379
380
  const el = createElement('div', {
380
381
  class: 'an-link-tooltip an-table-tooltip',
381
382
  role: 'toolbar',
382
- 'aria-label': 'Table actions',
383
+ 'aria-label': L.ariaLabel,
383
384
  });
384
385
  el.style.display = 'none';
385
386
 
386
387
  // Label
387
388
  this._label = createElement('span', { class: 'an-link-tooltip-url' });
388
- this._label.textContent = 'Table';
389
+ this._label.textContent = L.label;
389
390
  el.appendChild(this._label);
390
391
 
391
392
  el.appendChild(this._sep());
392
393
 
393
394
  // Select-cells toggle
394
- this._selectBtn = this._makeBtn(ICONS.selectCells, 'Select Cells', () => this._toggleSelectMode());
395
+ this._selectBtn = this._makeBtn(ICONS.selectCells, L.selectCells, () => this._toggleSelectMode());
395
396
  el.appendChild(this._selectBtn);
396
397
 
397
398
  el.appendChild(this._sep());
398
399
 
399
400
  // Row operations
400
- el.appendChild(this._makeBtn(ICONS.rowAbove, 'Add Row Above', () => this._addRow('above')));
401
- el.appendChild(this._makeBtn(ICONS.rowBelow, 'Add Row Below', () => this._addRow('below')));
402
- el.appendChild(this._makeBtn(ICONS.deleteRow, 'Delete Row', () => this._deleteRow()));
401
+ el.appendChild(this._makeBtn(ICONS.rowAbove, L.addRowAbove, () => this._addRow('above')));
402
+ el.appendChild(this._makeBtn(ICONS.rowBelow, L.addRowBelow, () => this._addRow('below')));
403
+ el.appendChild(this._makeBtn(ICONS.deleteRow, L.deleteRow, () => this._deleteRow()));
403
404
 
404
405
  el.appendChild(this._sep());
405
406
 
406
407
  // Column operations
407
- el.appendChild(this._makeBtn(ICONS.colLeft, 'Add Column Left', () => this._addColumn('left')));
408
- el.appendChild(this._makeBtn(ICONS.colRight, 'Add Column Right', () => this._addColumn('right')));
409
- el.appendChild(this._makeBtn(ICONS.deleteCol, 'Delete Column', () => this._deleteColumn()));
408
+ el.appendChild(this._makeBtn(ICONS.colLeft, L.addColumnLeft, () => this._addColumn('left')));
409
+ el.appendChild(this._makeBtn(ICONS.colRight, L.addColumnRight, () => this._addColumn('right')));
410
+ el.appendChild(this._makeBtn(ICONS.deleteCol, L.deleteColumn, () => this._deleteColumn()));
410
411
 
411
412
  el.appendChild(this._sep());
412
413
 
413
414
  // Merge cells
414
- el.appendChild(this._makeBtn(ICONS.mergeCells, 'Merge Cells', () => this._mergeCells()));
415
- el.appendChild(this._makeBtn(ICONS.unmergeCells, 'Unmerge Cells', () => this._unmergeCells()));
415
+ el.appendChild(this._makeBtn(ICONS.mergeCells, L.mergeCells, () => this._mergeCells()));
416
+ el.appendChild(this._makeBtn(ICONS.unmergeCells, L.unmergeCells, () => this._unmergeCells()));
416
417
 
417
418
  el.appendChild(this._sep());
418
419
 
419
420
  // Resize
420
- el.appendChild(this._makeBtn(ICONS.colWidth, 'Column Width', () => this._openSizePopover('col')));
421
- el.appendChild(this._makeBtn(ICONS.rowHeight, 'Row Height', () => this._openSizePopover('row')));
422
- el.appendChild(this._makeBtn(ICONS.tableBorder,'Table Border Width',() => this._openSizePopover('border')));
421
+ el.appendChild(this._makeBtn(ICONS.colWidth, L.columnWidth, () => this._openSizePopover('col')));
422
+ el.appendChild(this._makeBtn(ICONS.rowHeight, L.rowHeight, () => this._openSizePopover('row')));
423
+ el.appendChild(this._makeBtn(ICONS.tableBorder,L.tableBorderWidth, () => this._openSizePopover('border')));
423
424
 
424
425
  el.appendChild(this._sep());
425
426
 
426
427
  // Delete table (danger)
427
- el.appendChild(this._makeBtn(ICONS.deleteTable, 'Delete Table', () => this._deleteTable(), true));
428
+ el.appendChild(this._makeBtn(ICONS.deleteTable, L.deleteTable, () => this._deleteTable(), true));
428
429
 
429
430
  // Keep tooltip alive while hovering.
430
431
  // Don't schedule hide on mouseleave when the size popover is open —
@@ -909,9 +910,9 @@ export class TableTooltip {
909
910
 
910
911
  const actionsEl = createElement('div', { class: 'an-size-popover-actions' });
911
912
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
912
- cancelBtn.textContent = 'Cancel';
913
+ cancelBtn.textContent = this.context.locale.tooltips.table.cancelBtn;
913
914
  const applyBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
914
- applyBtn.textContent = 'Apply';
915
+ applyBtn.textContent = this.context.locale.tooltips.table.applyBtn;
915
916
  actionsEl.appendChild(cancelBtn);
916
917
  actionsEl.appendChild(applyBtn);
917
918
 
@@ -960,7 +961,7 @@ export class TableTooltip {
960
961
  ? (parseInt(firstCell.style.borderWidth, 10) ||
961
962
  parseInt(window.getComputedStyle(firstCell).borderWidth, 10) || 1)
962
963
  : 1;
963
- this._sizeTitleEl.textContent = 'Table Border Width (px)';
964
+ this._sizeTitleEl.textContent = this.context.locale.tooltips.table.tableBorderWidthPx;
964
965
  this._sizeInputEl.min = '0';
965
966
  this._sizeInputEl.max = '10';
966
967
  this._sizeInputEl.value = currentPx;
@@ -979,7 +980,9 @@ export class TableTooltip {
979
980
  const t = c.closest('table');
980
981
  return t && t === cell.closest('table');
981
982
  });
982
- this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
983
+ this._sizeTitleEl.textContent = isCol
984
+ ? this.context.locale.tooltips.table.columnWidthPx
985
+ : this.context.locale.tooltips.table.rowHeightPx;
983
986
  this._sizeInputEl.min = '1';
984
987
  this._sizeInputEl.max = '2000';
985
988
  this._sizeInputEl.value = isCol
@@ -182,9 +182,9 @@ export class Toolbar {
182
182
  const btn = createElement('button', {
183
183
  type: 'button',
184
184
  class: baseClass,
185
- title: def.tooltip || '',
185
+ title: this.context.locale.toolbar[def.name] || def.tooltip || '',
186
186
  'data-btn': def.name,
187
- 'aria-label': def.tooltip || def.name,
187
+ 'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
188
188
  'aria-haspopup': 'true',
189
189
  'aria-expanded': 'false',
190
190
  });
@@ -206,7 +206,7 @@ export class Toolbar {
206
206
  });
207
207
  const grid = createElement('div', { class: 'an-table-grid' });
208
208
  const label = createElement('div', { class: 'an-table-label' });
209
- label.textContent = 'Insert Table';
209
+ label.textContent = this.context.locale.toolbar.insertTableLabel || 'Insert Table';
210
210
 
211
211
  const cells = [];
212
212
  for (let r = 1; r <= ROWS; r++) {
@@ -232,7 +232,7 @@ export class Toolbar {
232
232
  const c = +cell.getAttribute('data-col');
233
233
  cell.classList.toggle('active', r <= rows && c <= cols);
234
234
  });
235
- label.textContent = (rows && cols) ? `${rows} × ${cols}` : 'Insert Table';
235
+ label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
236
236
  };
237
237
 
238
238
  const openPopup = () => {
@@ -307,9 +307,9 @@ export class Toolbar {
307
307
  const applyBtn = createElement('button', {
308
308
  type: 'button',
309
309
  class: `${baseClass} an-color-btn`,
310
- title: def.tooltip || '',
310
+ title: this.context.locale.toolbar[def.name] || def.tooltip || '',
311
311
  'data-btn': def.name,
312
- 'aria-label': def.tooltip || def.name,
312
+ 'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
313
313
  });
314
314
 
315
315
  const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
@@ -326,7 +326,9 @@ export class Toolbar {
326
326
  const arrowBtn = createElement('button', {
327
327
  type: 'button',
328
328
  class: `${baseClass} an-color-arrow`,
329
- title: `Choose ${def.name === 'foreColor' ? 'text' : 'highlight'} color`,
329
+ title: def.name === 'foreColor'
330
+ ? (this.context.locale.toolbar.chooseTextColor || 'Choose text color')
331
+ : (this.context.locale.toolbar.chooseHighlightColor || 'Choose highlight color'),
330
332
  'aria-haspopup': 'true',
331
333
  'aria-expanded': 'false',
332
334
  });
@@ -346,8 +348,8 @@ export class Toolbar {
346
348
  });
347
349
 
348
350
  const customRow = createElement('div', { class: 'an-color-custom' });
349
- const colorInput = createElement('input', { type: 'color', value: currentColor, title: 'Custom color' });
350
- const customLabel = createElement('span', {}, ['Custom color']);
351
+ const colorInput = createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' });
352
+ const customLabel = createElement('span', {}, [this.context.locale.toolbar.customColor || 'Custom color']);
351
353
  customRow.appendChild(colorInput);
352
354
  customRow.appendChild(customLabel);
353
355
 
@@ -491,19 +493,23 @@ export class Toolbar {
491
493
  const cls = def.selectClass ? `an-select ${def.selectClass}` : 'an-select';
492
494
  const select = createElement('select', {
493
495
  class: cls,
494
- title: def.tooltip || '',
496
+ title: this.context.locale.toolbar[def.name] || def.tooltip || '',
495
497
  'data-btn': def.name,
496
- 'aria-label': def.tooltip || def.name,
498
+ 'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
497
499
  });
498
500
 
499
501
  // Blank "placeholder" option (non-selectable header)
500
- const placeholderText = def.placeholder || 'Font';
502
+ const placeholderText = this.context.locale.toolbar[def.name + 'Placeholder'] || def.placeholder || 'Font';
501
503
  const placeholder = createElement('option', { value: '', disabled: '', hidden: '' }, [placeholderText]);
502
504
  select.appendChild(placeholder);
503
505
 
504
506
  items.forEach((item) => {
505
507
  const value = (typeof item === 'object') ? item.value : item;
506
- const label = (typeof item === 'object') ? item.label : item;
508
+ const label = (typeof item === 'object')
509
+ ? (def.name === 'paragraphStyle'
510
+ ? ((this.context.locale.toolbar.paragraphItems || {})[item.value] || item.label)
511
+ : item.label)
512
+ : item;
507
513
  const isHeader = (typeof item === 'object') && !!item.disabled;
508
514
  const attrs = { value };
509
515
  if (isHeader) attrs.disabled = '';
@@ -558,9 +564,9 @@ export class Toolbar {
558
564
  const btn = createElement('button', {
559
565
  type: 'button',
560
566
  class: classAttr,
561
- title: btnDef.tooltip || '',
567
+ title: this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || '',
562
568
  'data-btn': btnDef.name,
563
- 'aria-label': btnDef.tooltip || btnDef.name,
569
+ 'aria-label': this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || btnDef.name,
564
570
  });
565
571
 
566
572
  // Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
@@ -59,24 +59,25 @@ export class VideoDialog {
59
59
  // ---------------------------------------------------------------------------
60
60
 
61
61
  _buildDialog() {
62
+ const L = this.context.locale.videoDialog;
62
63
  const overlay = createElement('div', {
63
64
  class: 'an-dialog-overlay',
64
65
  role: 'dialog',
65
66
  'aria-modal': 'true',
66
- 'aria-label': 'Insert video',
67
+ 'aria-label': L.ariaLabel,
67
68
  });
68
69
  const box = createElement('div', { class: 'an-dialog-box' });
69
70
 
70
71
  const title = createElement('h3', { class: 'an-dialog-title' });
71
- title.textContent = 'Insert Video';
72
+ title.textContent = L.title;
72
73
 
73
74
  // URL input
74
75
  const urlLabel = createElement('label', { class: 'an-label' });
75
- urlLabel.textContent = 'Video URL';
76
+ urlLabel.textContent = L.videoUrl;
76
77
  const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
77
78
  type: 'url',
78
79
  class: 'an-input',
79
- placeholder: 'YouTube, Vimeo, or direct .mp4 URL',
80
+ placeholder: L.urlPlaceholder,
80
81
  autocomplete: 'off',
81
82
  }));
82
83
  this._urlInput = urlInput;
@@ -87,7 +88,7 @@ export class VideoDialog {
87
88
 
88
89
  // Width
89
90
  const widthLabel = createElement('label', { class: 'an-label' });
90
- widthLabel.textContent = 'Width (px)';
91
+ widthLabel.textContent = L.widthLabel;
91
92
  const widthInput = /** @type {HTMLInputElement} */ (createElement('input', {
92
93
  type: 'number',
93
94
  class: 'an-input',
@@ -101,9 +102,9 @@ export class VideoDialog {
101
102
  // Buttons
102
103
  const btnRow = createElement('div', { class: 'an-dialog-actions' });
103
104
  const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
104
- insertBtn.textContent = 'Insert';
105
+ insertBtn.textContent = L.insertBtn;
105
106
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
106
- cancelBtn.textContent = 'Cancel';
107
+ cancelBtn.textContent = L.cancelBtn;
107
108
  btnRow.appendChild(insertBtn);
108
109
  btnRow.appendChild(cancelBtn);
109
110
 
@@ -113,7 +114,7 @@ export class VideoDialog {
113
114
  // Live URL hint
114
115
  const d0 = on(urlInput, 'input', () => {
115
116
  const info = this._parseVideoUrl(urlInput.value.trim());
116
- hintEl.textContent = info ? `Detected: ${info.type}` : (urlInput.value ? 'Unknown format — will try direct video embed' : '');
117
+ hintEl.textContent = info ? this.context.locale.videoDialog.detected(info.type) : (urlInput.value ? this.context.locale.videoDialog.unknownFormat : '');
117
118
  });
118
119
 
119
120
  const d1 = on(insertBtn, 'click', () => this._onInsert());
@@ -140,7 +141,7 @@ export class VideoDialog {
140
141
 
141
142
  const html = this._buildEmbedHtml(rawUrl, width);
142
143
  if (!html) {
143
- this._hintEl.textContent = 'Invalid URL — please enter a valid video link.';
144
+ this._hintEl.textContent = this.context.locale.videoDialog.invalidUrl;
144
145
  this._urlInput.focus();
145
146
  return;
146
147
  }