autumnnote 1.6.7 → 1.8.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.
@@ -30,6 +30,8 @@ export class FindReplace extends BaseDialog {
30
30
  this._matches = [];
31
31
  this._currentIndex = -1;
32
32
  this._caseSensitive = false;
33
+ this._useRegex = false;
34
+ this._wholeWord = false;
33
35
  /** @type {'find'|'replace'} */
34
36
  this._mode = 'find';
35
37
 
@@ -37,6 +39,8 @@ export class FindReplace extends BaseDialog {
37
39
  this._queryRegex = null;
38
40
  this._lastQuery = null;
39
41
  this._lastCaseSensitive = null;
42
+ this._lastUseRegex = null;
43
+ this._lastWholeWord = null;
40
44
 
41
45
  this._focusTimer = null;
42
46
  }
@@ -169,6 +173,22 @@ export class FindReplace extends BaseDialog {
169
173
  });
170
174
  caseBtn.textContent = 'Aa';
171
175
 
176
+ const regexBtn = createElement('button', {
177
+ type: 'button',
178
+ class: 'an-fr-icon-btn',
179
+ title: L.useRegex,
180
+ 'aria-label': L.useRegex,
181
+ });
182
+ regexBtn.textContent = '.*';
183
+
184
+ const wholeWordBtn = createElement('button', {
185
+ type: 'button',
186
+ class: 'an-fr-icon-btn',
187
+ title: L.wholeWord,
188
+ 'aria-label': L.wholeWord,
189
+ });
190
+ wholeWordBtn.innerHTML = `<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="7" width="18" height="10" rx="2"/><line x1="7" y1="21" x2="7" y2="17"/><line x1="17" y1="21" x2="17" y2="17"/></svg>`;
191
+
172
192
  const prevBtn = createElement('button', {
173
193
  type: 'button',
174
194
  class: 'an-fr-icon-btn',
@@ -188,7 +208,7 @@ export class FindReplace extends BaseDialog {
188
208
  const counter = createElement('span', { class: 'an-fr-counter' });
189
209
  this._counterEl = counter;
190
210
 
191
- searchBar.append(findInput, caseCheckbox, caseBtn, prevBtn, nextBtn, counter);
211
+ searchBar.append(findInput, caseCheckbox, caseBtn, regexBtn, wholeWordBtn, prevBtn, nextBtn, counter);
192
212
  box.appendChild(searchBar);
193
213
 
194
214
  // ---- Replace row: [input] [Replace] [All] (hidden by default) ----
@@ -249,7 +269,21 @@ export class FindReplace extends BaseDialog {
249
269
  this._replace();
250
270
  }
251
271
  });
252
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11);
272
+ const dRegex = on(regexBtn, 'click', () => {
273
+ this._useRegex = !this._useRegex;
274
+ regexBtn.classList.toggle('an-fr-icon-btn--active', this._useRegex);
275
+ this._queryRegex = null; // force recompile
276
+ this._lastQuery = null;
277
+ this._onSearch();
278
+ });
279
+ const dWholeWord = on(wholeWordBtn, 'click', () => {
280
+ this._wholeWord = !this._wholeWord;
281
+ wholeWordBtn.classList.toggle('an-fr-icon-btn--active', this._wholeWord);
282
+ this._queryRegex = null;
283
+ this._lastQuery = null;
284
+ this._onSearch();
285
+ });
286
+ this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, dRegex, dWholeWord);
253
287
 
254
288
  return overlay;
255
289
  }
@@ -324,14 +358,24 @@ export class FindReplace extends BaseDialog {
324
358
  */
325
359
  _findRawMatches(query, root) {
326
360
  const results = [];
327
- // Reuse compiled regex when query and case-sensitivity haven't changed
328
- if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive) {
361
+ // Reuse compiled regex when query, case-sensitivity, regex mode, and whole-word haven't changed
362
+ if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive || this._lastUseRegex !== this._useRegex || this._lastWholeWord !== this._wholeWord) {
329
363
  const flags = this._caseSensitive ? 'g' : 'gi';
330
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
331
- this._queryRegex = new RegExp(escaped, flags);
364
+ try {
365
+ let pattern = this._useRegex
366
+ ? query
367
+ : query.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
368
+ if (this._wholeWord) pattern = `\\b${pattern}\\b`;
369
+ this._queryRegex = new RegExp(pattern, flags);
370
+ } catch (_) {
371
+ this._queryRegex = null;
372
+ }
332
373
  this._lastQuery = query;
333
374
  this._lastCaseSensitive = this._caseSensitive;
375
+ this._lastUseRegex = this._useRegex;
376
+ this._lastWholeWord = this._wholeWord;
334
377
  }
378
+ if (!this._queryRegex) return results;
335
379
  const re = this._queryRegex;
336
380
 
337
381
  // Cap results to prevent blocking the main thread on very large documents
@@ -204,6 +204,7 @@ export class ImageResizer {
204
204
  const isCorner = pos.length === 2; // 'nw','ne','se','sw'
205
205
 
206
206
  const editable = this.context.layoutInfo.editable;
207
+ const minSz = this.context.options?.minImageSize ?? 20;
207
208
  let _raf = null; // rAF handle — ensures at most one write per paint frame
208
209
 
209
210
  const onMove = (me) => {
@@ -218,19 +219,19 @@ export class ImageResizer {
218
219
  let newW = startW;
219
220
  let newH = startH;
220
221
 
221
- if (pos.includes('e')) newW = Math.max(20, startW + dx);
222
- if (pos.includes('w')) newW = Math.max(20, startW - dx);
223
- if (pos.includes('s')) newH = Math.max(20, startH + dy);
224
- if (pos.includes('n')) newH = Math.max(20, startH - dy);
222
+ if (pos.includes('e')) newW = Math.max(minSz, startW + dx);
223
+ if (pos.includes('w')) newW = Math.max(minSz, startW - dx);
224
+ if (pos.includes('s')) newH = Math.max(minSz, startH + dy);
225
+ if (pos.includes('n')) newH = Math.max(minSz, startH - dy);
225
226
 
226
227
  newW = Math.min(newW, maxW);
227
228
 
228
229
  if (isCorner) {
229
230
  if (Math.abs(dx) >= Math.abs(dy)) {
230
- newH = Math.max(20, Math.round(newW / aspectRatio));
231
+ newH = Math.max(minSz, Math.round(newW / aspectRatio));
231
232
  } else {
232
- newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
233
- newH = Math.max(20, Math.round(newW / aspectRatio));
233
+ newW = Math.min(Math.max(minSz, Math.round(newH * aspectRatio)), maxW);
234
+ newH = Math.max(minSz, Math.round(newW / aspectRatio));
234
235
  }
235
236
  }
236
237
 
@@ -269,14 +269,21 @@ export class Mention {
269
269
  this._query = query;
270
270
  clearTimeout(this._debounceTimer);
271
271
  this._debounceTimer = setTimeout(() => {
272
- this._cfg.onSearch(this._query, (items) => {
272
+ const cb = (items) => {
273
273
  if (!Array.isArray(items) || items.length === 0) {
274
274
  this._hideDropdown();
275
275
  return;
276
276
  }
277
277
  this._renderItems(items);
278
278
  this._showDropdown();
279
- });
279
+ };
280
+ const result = this._cfg.onSearch(this._query, cb);
281
+ if (result && typeof result.then === 'function') {
282
+ result.then(cb).catch((err) => {
283
+ this._hideDropdown();
284
+ if (typeof this._cfg.onError === 'function') this._cfg.onError(err);
285
+ });
286
+ }
280
287
  }, this._cfg.debounce);
281
288
  }
282
289
 
@@ -90,9 +90,9 @@ export class Statusbar {
90
90
  }
91
91
 
92
92
  // Counters
93
- this._wordCountEl = createElement('span', { class: 'an-word-count' });
94
- this._charCountEl = createElement('span', { class: 'an-char-count' });
95
- const info = createElement('div', { class: 'an-status-info' });
93
+ this._wordCountEl = createElement('span', { class: 'an-word-count', role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true' });
94
+ this._charCountEl = createElement('span', { class: 'an-char-count', 'aria-live': 'polite', 'aria-atomic': 'true' });
95
+ const info = createElement('div', { class: 'an-status-info', 'aria-label': 'Editor statistics' });
96
96
  info.appendChild(this._wordCountEl);
97
97
  info.appendChild(this._charCountEl);
98
98
  this.el.appendChild(info);
@@ -116,6 +116,16 @@ const ICONS = {
116
116
  deleteTable: `<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="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="16" y1="16" x2="22" y2="22" stroke="#ef4444"/><line x1="22" y1="16" x2="16" y2="22" stroke="#ef4444"/></svg>`,
117
117
  selectCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4 L4 20 L9 15 L12 21 L14 20 L11 14 L17 14 Z" fill="currentColor" opacity="0.15"/><path d="M4 4 L4 20 L9 15 L12 21 L14 20 L11 14 L17 14 Z"/></svg>`,
118
118
  cellShade: `<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="M19 11L8.93 3.36a1 1 0 0 0-1.29.08L3.22 7.8a1 1 0 0 0-.07 1.29L11 20"/><path d="m5 14 5-5"/><path d="M22 22a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2c0-1.5 2.5-5 3.5-5s3.5 3.5 3.5 5z"/></svg>`,
119
+ borderColor: `<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="14" rx="1"/><line x1="3" y1="10" x2="21" y2="10" stroke-width="1.5"/><line x1="12" y1="3" x2="12" y2="17" stroke-width="1.5"/><path d="M3 21h18" stroke-width="3"/></svg>`,
120
+ alignLeft: `<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"><line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/></svg>`,
121
+ alignCenter: `<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"><line x1="21" y1="6" x2="3" y2="6"/><line x1="17" y1="12" x2="7" y2="12"/><line x1="19" y1="18" x2="5" y2="18"/></svg>`,
122
+ alignRight: `<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"><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="9" y2="12"/><line x1="21" y1="18" x2="7" y2="18"/></svg>`,
123
+ alignJustify: `<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"><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="3" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/></svg>`,
124
+ headerRow: `<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="18" rx="1"/><rect x="3" y="3" width="18" height="8" rx="1" fill="currentColor" opacity="0.2"/><line x1="3" y1="11" x2="21" y2="11"/><line x1="3" y1="16" x2="21" y2="16"/><line x1="9" y1="11" x2="9" y2="21"/><line x1="15" y1="11" x2="15" y2="21"/></svg>`,
125
+ sortAsc: `<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"><line x1="4" y1="6" x2="11" y2="6"/><line x1="4" y1="12" x2="11" y2="12"/><line x1="4" y1="18" x2="13" y2="18"/><path d="M15 9l3-3 3 3"/><line x1="18" y1="6" x2="18" y2="18"/></svg>`,
126
+ sortDesc: `<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"><line x1="4" y1="6" x2="13" y2="6"/><line x1="4" y1="12" x2="11" y2="12"/><line x1="4" y1="18" x2="11" y2="18"/><path d="M15 15l3 3 3-3"/><line x1="18" y1="6" x2="18" y2="18"/></svg>`,
127
+ exportCSV: `<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 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
128
+ cellPadding: `<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="18" rx="1"/><rect x="7" y="7" width="10" height="10" rx="0.5" stroke-dasharray="2 1.5"/></svg>`,
119
129
  };
120
130
 
121
131
  const SHADE_PRESETS = [
@@ -142,6 +152,11 @@ export class TableTooltip {
142
152
  this._shadePopover = null;
143
153
  this._shadeTitleEl = null;
144
154
  this._shadeColorStrip = null;
155
+ // Border color popover
156
+ this._borderColorPopover = null;
157
+ this._borderColorTitleEl = null;
158
+ this._borderColorStrip = null;
159
+ this._borderColorNoBtn = null;
145
160
  // Cell selection
146
161
  this._selectMode = false;
147
162
  this._selectedCells = [];
@@ -161,6 +176,9 @@ export class TableTooltip {
161
176
  this._shadePopover = this._buildCellShadePopover();
162
177
  document.body.appendChild(this._shadePopover);
163
178
 
179
+ this._borderColorPopover = this._buildBorderColorPopover();
180
+ document.body.appendChild(this._borderColorPopover);
181
+
164
182
  const editable = this.context.layoutInfo.editable;
165
183
  this._editable = editable;
166
184
 
@@ -220,7 +238,8 @@ export class TableTooltip {
220
238
  if (this._activeTable &&
221
239
  !this._activeTable.contains(et) &&
222
240
  !this._el.contains(et) &&
223
- !this._sizePopover?.contains(et)) {
241
+ !this._sizePopover?.contains(et) &&
242
+ !this._borderColorPopover?.contains(et)) {
224
243
  this._hide();
225
244
  }
226
245
  }),
@@ -398,6 +417,10 @@ export class TableTooltip {
398
417
  this._shadePopover.remove();
399
418
  }
400
419
  this._shadePopover = null;
420
+ if (this._borderColorPopover?.parentNode) {
421
+ this._borderColorPopover.remove();
422
+ }
423
+ this._borderColorPopover = null;
401
424
  }
402
425
 
403
426
  // ---------------------------------------------------------------------------
@@ -437,6 +460,8 @@ export class TableTooltip {
437
460
  el.appendChild(this._makeBtn(ICONS.colLeft, L.addColumnLeft, () => this._addColumn('left')));
438
461
  el.appendChild(this._makeBtn(ICONS.colRight, L.addColumnRight, () => this._addColumn('right')));
439
462
  el.appendChild(this._makeBtn(ICONS.deleteCol, L.deleteColumn, () => this._deleteColumn()));
463
+ el.appendChild(this._makeBtn(ICONS.sortAsc, L.sortAsc, () => this._sortColumn('asc')));
464
+ el.appendChild(this._makeBtn(ICONS.sortDesc, L.sortDesc, () => this._sortColumn('desc')));
440
465
 
441
466
  el.appendChild(this._sep());
442
467
 
@@ -446,6 +471,15 @@ export class TableTooltip {
446
471
 
447
472
  el.appendChild(this._sep());
448
473
 
474
+ // Cell text alignment
475
+ el.appendChild(this._makeBtn(ICONS.alignLeft, L.cellAlignLeft, () => this._applyCellAlign('left')));
476
+ el.appendChild(this._makeBtn(ICONS.alignCenter, L.cellAlignCenter, () => this._applyCellAlign('center')));
477
+ el.appendChild(this._makeBtn(ICONS.alignRight, L.cellAlignRight, () => this._applyCellAlign('right')));
478
+ el.appendChild(this._makeBtn(ICONS.alignJustify, L.cellAlignJustify, () => this._applyCellAlign('justify')));
479
+ el.appendChild(this._makeBtn(ICONS.headerRow, L.toggleHeaderRow, () => this._toggleHeaderRow()));
480
+
481
+ el.appendChild(this._sep());
482
+
449
483
  // Cell background shading — uses color-strip variant like foreColor/hiliteColor buttons
450
484
  const shadeBtn = createElement('button', {
451
485
  type: 'button',
@@ -471,12 +505,35 @@ export class TableTooltip {
471
505
  el.appendChild(this._makeBtn(ICONS.colWidth, L.columnWidth, () => this._openSizePopover('col')));
472
506
  el.appendChild(this._makeBtn(ICONS.rowHeight, L.rowHeight, () => this._openSizePopover('row')));
473
507
  el.appendChild(this._makeBtn(ICONS.tableBorder,L.tableBorderWidth, () => this._openSizePopover('border')));
508
+ el.appendChild(this._makeBtn(ICONS.cellPadding, L.cellPadding, () => this._openSizePopover('cellPadding')));
509
+
510
+ // Table border color button — color-strip variant like shade button
511
+ const borderColorBtn = createElement('button', {
512
+ type: 'button',
513
+ class: 'an-link-tooltip-btn an-link-tooltip-btn--shade',
514
+ title: L.tableBorderColor,
515
+ });
516
+ const borderColorSvgWrap = createElement('span', { class: 'an-bubble-btn-svg' });
517
+ borderColorSvgWrap.innerHTML = ICONS.borderColor;
518
+ const borderColorStrip = createElement('span', { class: 'an-link-tooltip-color-strip' });
519
+ borderColorBtn.appendChild(borderColorSvgWrap);
520
+ borderColorBtn.appendChild(borderColorStrip);
521
+ this._borderColorStrip = borderColorStrip;
522
+ this._disposers.push(on(borderColorBtn, 'click', (e) => {
523
+ e.preventDefault();
524
+ e.stopPropagation();
525
+ this._openBorderColorPopover();
526
+ }));
527
+ el.appendChild(borderColorBtn);
474
528
 
475
529
  el.appendChild(this._sep());
476
530
 
477
531
  // Delete table (danger)
478
532
  el.appendChild(this._makeBtn(ICONS.deleteTable, L.deleteTable, () => this._deleteTable(), true));
479
533
 
534
+ el.appendChild(this._sep());
535
+ el.appendChild(this._makeBtn(ICONS.exportCSV, L.exportCSV, () => this._exportTableCSV()));
536
+
480
537
  // Keep tooltip alive while hovering.
481
538
  // Don't schedule hide on mouseleave when the size popover is open —
482
539
  // the user is moving the mouse toward it.
@@ -486,6 +543,7 @@ export class TableTooltip {
486
543
  if (this._selectMode) return; // keep tooltip alive during cell selection
487
544
  if (this._sizePopover && this._sizePopover.style.display !== 'none') return;
488
545
  if (this._shadePopover && this._shadePopover.style.display !== 'none') return;
546
+ if (this._borderColorPopover && this._borderColorPopover.style.display !== 'none') return;
489
547
  this._scheduleHide();
490
548
  }),
491
549
  );
@@ -546,6 +604,7 @@ export class TableTooltip {
546
604
  if (!this._activeTable) return;
547
605
  this._el.style.display = 'flex';
548
606
  this._syncShadeStrip();
607
+ this._syncBorderColorStrip();
549
608
  // Defer: offsetWidth on a newly-visible element forces layout synchronously
550
609
  requestAnimationFrame(() => {
551
610
  if (this._activeTable) this._positionNear(this._activeTable);
@@ -571,6 +630,7 @@ export class TableTooltip {
571
630
  this._clearSelection();
572
631
  this._clearTimers();
573
632
  this._hideSizePopover();
633
+ this._hideBorderColorPopover();
574
634
  }
575
635
 
576
636
  _clearTimers() {
@@ -1048,6 +1108,22 @@ export class TableTooltip {
1048
1108
  }
1049
1109
  this.context.invoke('editor.afterCommand');
1050
1110
  };
1111
+ } else if (type === 'cellPadding') {
1112
+ const cells = this._getSelectedCells();
1113
+ const firstCell = cells[0] || this._getCell();
1114
+ const currentPad = firstCell
1115
+ ? (Number.parseInt(firstCell.style.padding, 10) ||
1116
+ Number.parseInt(firstCell.style.paddingTop, 10) || 4)
1117
+ : 4;
1118
+ this._sizeTitleEl.textContent = this.context.locale.tooltips.table.cellPaddingPx;
1119
+ this._sizeInputEl.min = '0';
1120
+ this._sizeInputEl.max = '40';
1121
+ this._sizeInputEl.value = String(currentPad);
1122
+ this._sizeApply = (val) => {
1123
+ const activeCells = this._getSelectedCells();
1124
+ activeCells.forEach((c) => { if (c) c.style.padding = `${val}px`; });
1125
+ this.context.invoke('editor.afterCommand');
1126
+ };
1051
1127
  } else {
1052
1128
  const isCol = type === 'col';
1053
1129
  const activeCells = this._getSelectedCells().filter((c) => {
@@ -1148,7 +1224,7 @@ export class TableTooltip {
1148
1224
  const customRow = createElement('div', { class: 'an-context-color-custom' });
1149
1225
  const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-shade-color-input', value: '#ffffff' }));
1150
1226
  const customLabel = createElement('span');
1151
- customLabel.textContent = 'Custom…';
1227
+ customLabel.textContent = this.context.locale.contextMenu.customColorLabel;
1152
1228
  this._disposers.push(on(colorInput, 'change', () => this._applyCellShade(colorInput.value)));
1153
1229
  customRow.appendChild(colorInput);
1154
1230
  customRow.appendChild(customLabel);
@@ -1211,4 +1287,213 @@ export class TableTooltip {
1211
1287
  this._hideCellShadePopover();
1212
1288
  this.context.invoke('editor.afterCommand');
1213
1289
  }
1290
+
1291
+ // ---------------------------------------------------------------------------
1292
+ // Table border color popover
1293
+ // ---------------------------------------------------------------------------
1294
+
1295
+ _buildBorderColorPopover() {
1296
+ const pop = createElement('div', { class: 'an-cell-shade-popover' });
1297
+ pop.style.display = 'none';
1298
+
1299
+ const title = createElement('div', { class: 'an-size-popover-title' });
1300
+ pop.appendChild(title);
1301
+ this._borderColorTitleEl = title;
1302
+
1303
+ const palette = createElement('div', { class: 'an-context-color-palette' });
1304
+ SHADE_PRESETS.forEach((color) => {
1305
+ const sw = createElement('div', { class: 'an-context-color-swatch', title: color });
1306
+ sw.style.background = color;
1307
+ this._disposers.push(on(sw, 'click', (e) => {
1308
+ e.stopPropagation();
1309
+ this._applyBorderColor(color);
1310
+ }));
1311
+ palette.appendChild(sw);
1312
+ });
1313
+ pop.appendChild(palette);
1314
+
1315
+ const noColorRow = createElement('div', { class: 'an-context-color-custom' });
1316
+ const noColorBtn = createElement('button', { type: 'button', class: 'an-shade-no-color' });
1317
+ this._disposers.push(on(noColorBtn, 'click', () => this._applyBorderColor('')));
1318
+ noColorRow.appendChild(noColorBtn);
1319
+ pop.appendChild(noColorRow);
1320
+ this._borderColorNoBtn = noColorBtn;
1321
+
1322
+ const customRow = createElement('div', { class: 'an-context-color-custom' });
1323
+ const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-shade-color-input', value: '#000000' }));
1324
+ const customLabel = createElement('span');
1325
+ customLabel.textContent = this.context.locale.contextMenu.customColorLabel;
1326
+ this._disposers.push(on(colorInput, 'change', () => this._applyBorderColor(colorInput.value)));
1327
+ customRow.appendChild(colorInput);
1328
+ customRow.appendChild(customLabel);
1329
+ pop.appendChild(customRow);
1330
+
1331
+ this._disposers.push(
1332
+ on(pop, 'mousedown', (e) => e.preventDefault()),
1333
+ on(pop, 'mouseenter', () => this._clearTimers()),
1334
+ on(pop, 'mouseleave', () => this._scheduleHide()),
1335
+ on(document, 'click', (e) => {
1336
+ const et = /** @type {Node} */ (e.target);
1337
+ if (this._borderColorPopover &&
1338
+ this._borderColorPopover.style.display !== 'none' &&
1339
+ !this._borderColorPopover.contains(et) &&
1340
+ !this._el?.contains(et)) {
1341
+ this._hideBorderColorPopover();
1342
+ }
1343
+ }),
1344
+ );
1345
+
1346
+ return pop;
1347
+ }
1348
+
1349
+ _openBorderColorPopover() {
1350
+ if (!this._borderColorPopover) return;
1351
+ const L = this.context.locale.tooltips.table;
1352
+ if (this._borderColorTitleEl) this._borderColorTitleEl.textContent = L.tableBorderColor;
1353
+ if (this._borderColorNoBtn) this._borderColorNoBtn.textContent = L.noBorderColor;
1354
+
1355
+ this._borderColorPopover.style.display = 'block';
1356
+ requestAnimationFrame(() => {
1357
+ if (!this._borderColorPopover || !this._el) return;
1358
+ const pw = this._borderColorPopover.offsetWidth || 170;
1359
+ const ph = this._borderColorPopover.offsetHeight || 120;
1360
+ const tipRect = this._el.getBoundingClientRect();
1361
+ let left = tipRect.left;
1362
+ let top = tipRect.bottom + 6;
1363
+ if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
1364
+ if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
1365
+ this._borderColorPopover.style.left = `${Math.max(8, left)}px`;
1366
+ this._borderColorPopover.style.top = `${Math.max(8, top)}px`;
1367
+ });
1368
+ }
1369
+
1370
+ _hideBorderColorPopover() {
1371
+ if (this._borderColorPopover) this._borderColorPopover.style.display = 'none';
1372
+ }
1373
+
1374
+ _applyBorderColor(color) {
1375
+ const table = this._activeTable;
1376
+ if (!table) return;
1377
+ Array.from(table.querySelectorAll('td, th')).forEach((c) => { c.style.borderColor = color; });
1378
+ if (this._borderColorStrip) {
1379
+ this._borderColorStrip.style.background = color || 'transparent';
1380
+ }
1381
+ this._hideBorderColorPopover();
1382
+ this.context.invoke('editor.afterCommand');
1383
+ }
1384
+
1385
+ _syncBorderColorStrip() {
1386
+ if (!this._borderColorStrip || !this._el || this._el.style.display === 'none') return;
1387
+ const firstCell = this._activeTable?.querySelector('td, th');
1388
+ this._borderColorStrip.style.background = firstCell?.style.borderColor || 'transparent';
1389
+ }
1390
+
1391
+ // ---------------------------------------------------------------------------
1392
+ // Cell text alignment
1393
+ // ---------------------------------------------------------------------------
1394
+
1395
+ _applyCellAlign(align) {
1396
+ const cells = this._getSelectedCells();
1397
+ cells.forEach((c) => { if (c) c.style.textAlign = align; });
1398
+ this.context.invoke('editor.afterCommand');
1399
+ }
1400
+
1401
+ // ---------------------------------------------------------------------------
1402
+ // Toggle header row
1403
+ // ---------------------------------------------------------------------------
1404
+
1405
+ _toggleHeaderRow() {
1406
+ const table = this._activeTable;
1407
+ if (!table) return;
1408
+ const firstRow = table.querySelector('tr');
1409
+ if (!firstRow) return;
1410
+
1411
+ const isInThead = firstRow.closest('thead') !== null;
1412
+
1413
+ if (isInThead) {
1414
+ // Convert th → td and move row to tbody
1415
+ let tbody = table.querySelector('tbody');
1416
+ if (!tbody) {
1417
+ tbody = document.createElement('tbody');
1418
+ table.appendChild(tbody);
1419
+ }
1420
+ Array.from(firstRow.cells).forEach((cell) => {
1421
+ const td = document.createElement('td');
1422
+ td.innerHTML = cell.innerHTML;
1423
+ td.style.cssText = cell.style.cssText;
1424
+ firstRow.replaceChild(td, cell);
1425
+ });
1426
+ tbody.insertBefore(firstRow, tbody.firstChild);
1427
+ const thead = table.querySelector('thead');
1428
+ if (thead && thead.rows.length === 0) thead.remove();
1429
+ } else {
1430
+ // Convert td → th and move row to thead
1431
+ let thead = table.querySelector('thead');
1432
+ if (!thead) {
1433
+ thead = document.createElement('thead');
1434
+ table.insertBefore(thead, table.firstChild);
1435
+ }
1436
+ Array.from(firstRow.cells).forEach((cell) => {
1437
+ const th = document.createElement('th');
1438
+ th.innerHTML = cell.innerHTML;
1439
+ th.style.cssText = cell.style.cssText;
1440
+ firstRow.replaceChild(th, cell);
1441
+ });
1442
+ thead.appendChild(firstRow);
1443
+ }
1444
+
1445
+ this.context.invoke('editor.afterCommand');
1446
+ }
1447
+
1448
+ _sortColumn(direction) {
1449
+ const table = this._activeTable;
1450
+ if (!table) return;
1451
+ const cell = this._getCell();
1452
+ if (!cell) return;
1453
+ const colIdx = getVisualColIndex(cell);
1454
+ if (colIdx === -1) return;
1455
+ const tbody = table.querySelector('tbody') || table;
1456
+ const rows = Array.from(tbody.querySelectorAll(':scope > tr'));
1457
+ if (rows.length < 2) return;
1458
+ rows.sort((a, b) => {
1459
+ const aCell = getCellAtVisualCol(a, colIdx);
1460
+ const bCell = getCellAtVisualCol(b, colIdx);
1461
+ const aText = (aCell?.textContent || '').trim();
1462
+ const bText = (bCell?.textContent || '').trim();
1463
+ const aNum = parseFloat(aText);
1464
+ const bNum = parseFloat(bText);
1465
+ if (!isNaN(aNum) && !isNaN(bNum)) {
1466
+ return direction === 'asc' ? aNum - bNum : bNum - aNum;
1467
+ }
1468
+ return direction === 'asc'
1469
+ ? aText.localeCompare(bText)
1470
+ : bText.localeCompare(aText);
1471
+ });
1472
+ rows.forEach((row) => tbody.appendChild(row));
1473
+ this.context.invoke('editor.afterCommand');
1474
+ }
1475
+
1476
+ _exportTableCSV() {
1477
+ const table = this._activeTable;
1478
+ if (!table) return;
1479
+ const rows = Array.from(table.querySelectorAll('tr'));
1480
+ const csv = rows.map((row) =>
1481
+ Array.from(row.querySelectorAll('td, th'))
1482
+ .map((cell) => {
1483
+ const text = (cell.textContent || '').trim().replace(/"/g, '""');
1484
+ return `"${text}"`;
1485
+ })
1486
+ .join(',')
1487
+ ).join('\n');
1488
+ const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
1489
+ const url = URL.createObjectURL(blob);
1490
+ const a = document.createElement('a');
1491
+ a.href = url;
1492
+ a.download = 'table.csv';
1493
+ a.style.display = 'none';
1494
+ document.body.appendChild(a);
1495
+ a.click();
1496
+ a.remove();
1497
+ URL.revokeObjectURL(url);
1498
+ }
1214
1499
  }
@@ -70,11 +70,14 @@ export function renderLayout(targetEl, options) {
70
70
 
71
71
  container.appendChild(editable);
72
72
 
73
- // Apply dark theme — also add to body so floating elements (dialogs, tooltips,
74
- // popovers) appended to document.body inherit the dark CSS rules.
73
+ // Apply theme — also add to body so floating elements (dialogs, tooltips,
74
+ // popovers) appended to document.body inherit the CSS rules.
75
75
  if (options.theme === 'dark') {
76
76
  container.classList.add('an-theme-dark');
77
77
  document.body.classList.add('an-theme-dark');
78
+ } else if (options.theme === 'auto') {
79
+ container.classList.add('an-theme-auto');
80
+ document.body.classList.add('an-theme-auto');
78
81
  }
79
82
 
80
83
  // Read-only mode