overtype 2.1.0 → 2.2.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.
package/src/overtype.js CHANGED
@@ -7,10 +7,69 @@
7
7
  import { MarkdownParser } from './parser.js';
8
8
  import { ShortcutsManager } from './shortcuts.js';
9
9
  import { generateStyles } from './styles.js';
10
- import { getTheme, mergeTheme, solar, themeToCSSVars } from './themes.js';
10
+ import { getTheme, mergeTheme, solar, themeToCSSVars, resolveAutoTheme } from './themes.js';
11
11
  import { Toolbar } from './toolbar.js';
12
12
  import { LinkTooltip } from './link-tooltip.js';
13
- import { defaultToolbarButtons } from './toolbar-buttons.js';
13
+ import { defaultToolbarButtons, toolbarButtons as builtinToolbarButtons } from './toolbar-buttons.js';
14
+
15
+ /**
16
+ * Build action map from toolbar button configurations
17
+ * @param {Array} buttons - Array of button config objects
18
+ * @returns {Object} Map of actionId -> action function
19
+ */
20
+ function buildActionsMap(buttons) {
21
+ const map = {};
22
+ (buttons || []).forEach((btn) => {
23
+ if (!btn || btn.name === 'separator') return;
24
+ const id = btn.actionId || btn.name;
25
+ if (btn.action) {
26
+ map[id] = btn.action;
27
+ }
28
+ });
29
+ return map;
30
+ }
31
+
32
+ /**
33
+ * Normalize toolbar buttons for comparison
34
+ * @param {Array|null} buttons
35
+ * @returns {Array|null}
36
+ */
37
+ function normalizeButtons(buttons) {
38
+ const list = buttons || defaultToolbarButtons;
39
+ if (!Array.isArray(list)) return null;
40
+ return list.map((btn) => ({
41
+ name: btn?.name || null,
42
+ actionId: btn?.actionId || btn?.name || null,
43
+ icon: btn?.icon || null,
44
+ title: btn?.title || null
45
+ }));
46
+ }
47
+
48
+ /**
49
+ * Determine if toolbar button configuration changed
50
+ * @param {Array|null} prevButtons
51
+ * @param {Array|null} nextButtons
52
+ * @returns {boolean}
53
+ */
54
+ function toolbarButtonsChanged(prevButtons, nextButtons) {
55
+ const prev = normalizeButtons(prevButtons);
56
+ const next = normalizeButtons(nextButtons);
57
+
58
+ if (prev === null || next === null) return prev !== next;
59
+ if (prev.length !== next.length) return true;
60
+
61
+ for (let i = 0; i < prev.length; i++) {
62
+ const a = prev[i];
63
+ const b = next[i];
64
+ if (a.name !== b.name ||
65
+ a.actionId !== b.actionId ||
66
+ a.icon !== b.icon ||
67
+ a.title !== b.title) {
68
+ return true;
69
+ }
70
+ }
71
+ return false;
72
+ }
14
73
 
15
74
  /**
16
75
  * OverType Editor Class
@@ -21,6 +80,11 @@ class OverType {
21
80
  static stylesInjected = false;
22
81
  static globalListenersInitialized = false;
23
82
  static instanceCount = 0;
83
+ static _autoMediaQuery = null;
84
+ static _autoMediaListener = null;
85
+ static _autoInstances = new Set();
86
+ static _globalAutoTheme = false;
87
+ static _globalAutoCustomColors = null;
24
88
 
25
89
  /**
26
90
  * Constructor - Always returns an array of instances
@@ -97,9 +161,16 @@ class OverType {
97
161
  this._buildFromScratch();
98
162
  }
99
163
 
164
+ if (this.instanceTheme === 'auto') {
165
+ this.setTheme('auto');
166
+ }
167
+
100
168
  // Setup shortcuts manager
101
169
  this.shortcuts = new ShortcutsManager(this);
102
170
 
171
+ // Build action map from toolbar buttons (works whether or not toolbar UI is shown)
172
+ this._rebuildActionsMap();
173
+
103
174
  // Setup link tooltip
104
175
  this.linkTooltip = new LinkTooltip(this);
105
176
 
@@ -164,7 +235,8 @@ class OverType {
164
235
  toolbarButtons: null, // Defaults to defaultToolbarButtons if toolbar: true
165
236
  statsFormatter: null,
166
237
  smartLists: true, // Enable smart list continuation
167
- codeHighlighter: null // Per-instance code highlighter
238
+ codeHighlighter: null, // Per-instance code highlighter
239
+ spellcheck: false // Browser spellcheck (disabled by default)
168
240
  };
169
241
 
170
242
  // Remove theme and colors from options - these are now global
@@ -352,9 +424,16 @@ class OverType {
352
424
  this.preview.className = 'overtype-preview';
353
425
  this.preview.setAttribute('aria-hidden', 'true');
354
426
 
427
+ // Create placeholder shim
428
+ this.placeholderEl = document.createElement('div');
429
+ this.placeholderEl.className = 'overtype-placeholder';
430
+ this.placeholderEl.setAttribute('aria-hidden', 'true');
431
+ this.placeholderEl.textContent = this.options.placeholder;
432
+
355
433
  // Assemble DOM
356
434
  this.wrapper.appendChild(this.textarea);
357
435
  this.wrapper.appendChild(this.preview);
436
+ this.wrapper.appendChild(this.placeholderEl);
358
437
 
359
438
  // No need to prevent link clicks - pointer-events handles this
360
439
 
@@ -389,7 +468,7 @@ class OverType {
389
468
  this.textarea.setAttribute('autocomplete', 'off');
390
469
  this.textarea.setAttribute('autocorrect', 'off');
391
470
  this.textarea.setAttribute('autocapitalize', 'off');
392
- this.textarea.setAttribute('spellcheck', 'false');
471
+ this.textarea.setAttribute('spellcheck', String(this.options.spellcheck));
393
472
  this.textarea.setAttribute('data-gramm', 'false');
394
473
  this.textarea.setAttribute('data-gramm_editor', 'false');
395
474
  this.textarea.setAttribute('data-enable-grammarly', 'false');
@@ -400,12 +479,22 @@ class OverType {
400
479
  * @private
401
480
  */
402
481
  _createToolbar() {
403
- // Use provided toolbarButtons or default to defaultToolbarButtons
404
- const toolbarButtons = this.options.toolbarButtons || defaultToolbarButtons;
482
+ let toolbarButtons = this.options.toolbarButtons || defaultToolbarButtons;
483
+
484
+ if (this.options.fileUpload?.enabled && !toolbarButtons.some(b => b?.name === 'upload')) {
485
+ const viewModeIdx = toolbarButtons.findIndex(b => b?.name === 'viewMode');
486
+ if (viewModeIdx !== -1) {
487
+ toolbarButtons = [...toolbarButtons];
488
+ toolbarButtons.splice(viewModeIdx, 0, builtinToolbarButtons.separator, builtinToolbarButtons.upload);
489
+ } else {
490
+ toolbarButtons = [...toolbarButtons, builtinToolbarButtons.separator, builtinToolbarButtons.upload];
491
+ }
492
+ }
405
493
 
406
494
  this.toolbar = new Toolbar(this, { toolbarButtons });
407
495
  this.toolbar.create();
408
496
 
497
+
409
498
  // Store listener references for cleanup
410
499
  this._toolbarSelectionListener = () => {
411
500
  if (this.toolbar) {
@@ -438,6 +527,26 @@ class OverType {
438
527
  }
439
528
  }
440
529
 
530
+ /**
531
+ * Rebuild the action map from current toolbar button configuration
532
+ * Called during init and reinit to keep shortcuts in sync with toolbar buttons
533
+ * @private
534
+ */
535
+ _rebuildActionsMap() {
536
+ // Always start with default actions (shortcuts always work regardless of toolbar config)
537
+ this.actionsById = buildActionsMap(defaultToolbarButtons);
538
+
539
+ // Overlay custom toolbar actions (can add/override, but never remove core actions)
540
+ if (this.options.toolbarButtons) {
541
+ Object.assign(this.actionsById, buildActionsMap(this.options.toolbarButtons));
542
+ }
543
+
544
+ // Register upload action when file upload is enabled
545
+ if (this.options.fileUpload?.enabled) {
546
+ Object.assign(this.actionsById, buildActionsMap([builtinToolbarButtons.upload]));
547
+ }
548
+ }
549
+
441
550
  /**
442
551
  * Apply options to the editor
443
552
  * @private
@@ -452,6 +561,8 @@ class OverType {
452
561
  if (this.options.autoResize) {
453
562
  if (!this.container.classList.contains('overtype-auto-resize')) {
454
563
  this._setupAutoResize();
564
+ } else {
565
+ this._updateAutoHeight();
455
566
  }
456
567
  } else {
457
568
  // Ensure auto-resize class is removed
@@ -469,10 +580,135 @@ class OverType {
469
580
  this.toolbar = null;
470
581
  }
471
582
 
583
+ // Update placeholder text
584
+ if (this.placeholderEl) {
585
+ this.placeholderEl.textContent = this.options.placeholder;
586
+ }
587
+
588
+ // Setup or remove file upload
589
+ if (this.options.fileUpload && !this.fileUploadInitialized) {
590
+ this._initFileUpload();
591
+ } else if (!this.options.fileUpload && this.fileUploadInitialized) {
592
+ this._destroyFileUpload();
593
+ }
594
+
472
595
  // Update preview with initial content
473
596
  this.updatePreview();
474
597
  }
475
598
 
599
+ _initFileUpload() {
600
+ const options = this.options.fileUpload;
601
+ if (!options || !options.enabled) return;
602
+
603
+ options.maxSize = options.maxSize || 10 * 1024 * 1024;
604
+ options.mimeTypes = options.mimeTypes || [];
605
+ options.batch = options.batch || false;
606
+ if (!options.onInsertFile || typeof options.onInsertFile !== 'function') {
607
+ console.warn('OverType: fileUpload.onInsertFile callback is required for file uploads.');
608
+ return;
609
+ }
610
+
611
+ this._fileUploadCounter = 0;
612
+ this._boundHandleFilePaste = this._handleFilePaste.bind(this);
613
+ this._boundHandleFileDrop = this._handleFileDrop.bind(this);
614
+ this._boundHandleDragOver = this._handleDragOver.bind(this);
615
+
616
+ this.textarea.addEventListener('paste', this._boundHandleFilePaste);
617
+ this.textarea.addEventListener('drop', this._boundHandleFileDrop);
618
+ this.textarea.addEventListener('dragover', this._boundHandleDragOver);
619
+
620
+ this.fileUploadInitialized = true;
621
+ }
622
+
623
+ _handleFilePaste(e) {
624
+ if (!e?.clipboardData?.files?.length) return;
625
+ e.preventDefault();
626
+ this._handleDataTransfer(e.clipboardData);
627
+ }
628
+
629
+ _handleFileDrop(e) {
630
+ if (!e?.dataTransfer?.files?.length) return;
631
+ e.preventDefault();
632
+ this._handleDataTransfer(e.dataTransfer);
633
+ }
634
+
635
+ _handleDataTransfer(dataTransfer) {
636
+ const files = [];
637
+ for (const file of dataTransfer.files) {
638
+ if (file.size > this.options.fileUpload.maxSize) continue;
639
+ if (this.options.fileUpload.mimeTypes.length > 0
640
+ && !this.options.fileUpload.mimeTypes.includes(file.type)) continue;
641
+
642
+ const id = ++this._fileUploadCounter;
643
+ const prefix = file.type.startsWith('image/') ? '!' : '';
644
+ const placeholder = `${prefix}[Uploading ${file.name} (#${id})...]()`;
645
+ this.insertAtCursor(`${placeholder}\n`);
646
+
647
+ if (this.options.fileUpload.batch) {
648
+ files.push({ file, placeholder });
649
+ continue;
650
+ }
651
+
652
+ this.options.fileUpload.onInsertFile(file).then((text) => {
653
+ this.textarea.value = this.textarea.value.replace(placeholder, text);
654
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
655
+ }, (error) => {
656
+ console.error('OverType: File upload failed', error);
657
+ this.textarea.value = this.textarea.value.replace(placeholder, '[Upload failed]()');
658
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
659
+ });
660
+ }
661
+
662
+ if (this.options.fileUpload.batch && files.length > 0) {
663
+ this.options.fileUpload.onInsertFile(files.map(f => f.file)).then((result) => {
664
+ const texts = Array.isArray(result) ? result : [result];
665
+ texts.forEach((text, index) => {
666
+ this.textarea.value = this.textarea.value.replace(files[index].placeholder, text);
667
+ });
668
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
669
+ }, (error) => {
670
+ console.error('OverType: File upload failed', error);
671
+ files.forEach(({ placeholder }) => {
672
+ this.textarea.value = this.textarea.value.replace(placeholder, '[Upload failed]()');
673
+ });
674
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
675
+ });
676
+ }
677
+ }
678
+
679
+ _handleDragOver(e) {
680
+ e.preventDefault();
681
+ }
682
+
683
+ _destroyFileUpload() {
684
+ this.textarea.removeEventListener('paste', this._boundHandleFilePaste);
685
+ this.textarea.removeEventListener('drop', this._boundHandleFileDrop);
686
+ this.textarea.removeEventListener('dragover', this._boundHandleDragOver);
687
+ this._boundHandleFilePaste = null;
688
+ this._boundHandleFileDrop = null;
689
+ this._boundHandleDragOver = null;
690
+ this.fileUploadInitialized = false;
691
+ }
692
+
693
+ insertAtCursor(text) {
694
+ const start = this.textarea.selectionStart;
695
+ const end = this.textarea.selectionEnd;
696
+
697
+ let inserted = false;
698
+ try {
699
+ inserted = document.execCommand('insertText', false, text);
700
+ } catch (_) {}
701
+
702
+ if (!inserted) {
703
+ const before = this.textarea.value.slice(0, start);
704
+ const after = this.textarea.value.slice(end);
705
+ this.textarea.value = before + text + after;
706
+ this.textarea.setSelectionRange(start + text.length, start + text.length);
707
+ }
708
+
709
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
710
+ }
711
+
476
712
  /**
477
713
  * Update preview with parsed markdown
478
714
  */
@@ -486,7 +722,12 @@ class OverType {
486
722
 
487
723
  // Parse markdown
488
724
  const html = MarkdownParser.parse(text, activeLine, this.options.showActiveLineRaw, this.options.codeHighlighter, isPreviewMode);
489
- this.preview.innerHTML = html || '<span style="color: #808080;">Start typing...</span>';
725
+ this.preview.innerHTML = html;
726
+
727
+ // Show/hide placeholder shim
728
+ if (this.placeholderEl) {
729
+ this.placeholderEl.style.display = text ? 'none' : '';
730
+ }
490
731
 
491
732
  // Apply code block backgrounds
492
733
  this._applyCodeBlockBackgrounds();
@@ -807,13 +1048,50 @@ class OverType {
807
1048
  setValue(value) {
808
1049
  this.textarea.value = value;
809
1050
  this.updatePreview();
810
-
1051
+
811
1052
  // Update height if auto-resize is enabled
812
1053
  if (this.options.autoResize) {
813
1054
  this._updateAutoHeight();
814
1055
  }
815
1056
  }
816
1057
 
1058
+ /**
1059
+ * Execute an action by ID
1060
+ * Central dispatcher used by toolbar clicks, keyboard shortcuts, and programmatic calls
1061
+ * @param {string} actionId - The action identifier (e.g., 'toggleBold', 'insertLink')
1062
+ * @param {Event|null} event - Optional event that triggered the action
1063
+ * @returns {Promise<boolean>} Whether the action was executed successfully
1064
+ */
1065
+ async performAction(actionId, event = null) {
1066
+ const textarea = this.textarea;
1067
+ if (!textarea) return false;
1068
+
1069
+ const action = this.actionsById?.[actionId];
1070
+ if (!action) {
1071
+ console.warn(`OverType: Unknown action "${actionId}"`);
1072
+ return false;
1073
+ }
1074
+
1075
+ textarea.focus();
1076
+
1077
+ try {
1078
+ await action({
1079
+ editor: this,
1080
+ getValue: () => this.getValue(),
1081
+ setValue: (value) => this.setValue(value),
1082
+ event
1083
+ });
1084
+ // Note: actions are responsible for dispatching input event
1085
+ // This preserves behavior for direct consumers of toolbarButtons.*.action
1086
+ return true;
1087
+ } catch (error) {
1088
+ console.error(`OverType: Action "${actionId}" error:`, error);
1089
+ this.wrapper.dispatchEvent(new CustomEvent('button-error', {
1090
+ detail: { actionId, error }
1091
+ }));
1092
+ return false;
1093
+ }
1094
+ }
817
1095
 
818
1096
  /**
819
1097
  * Get the rendered HTML of the current content
@@ -882,39 +1160,89 @@ class OverType {
882
1160
  * @param {Object} options - New options to apply
883
1161
  */
884
1162
  reinit(options = {}) {
1163
+ const prevToolbarButtons = this.options?.toolbarButtons;
885
1164
  this.options = this._mergeOptions({ ...this.options, ...options });
1165
+ const toolbarNeedsRebuild = this.toolbar &&
1166
+ this.options.toolbar &&
1167
+ toolbarButtonsChanged(prevToolbarButtons, this.options.toolbarButtons);
1168
+
1169
+ // Rebuild action map in case toolbarButtons changed
1170
+ this._rebuildActionsMap();
1171
+
1172
+ if (toolbarNeedsRebuild) {
1173
+ this._cleanupToolbarListeners();
1174
+ this.toolbar.destroy();
1175
+ this.toolbar = null;
1176
+ this._createToolbar();
1177
+ }
1178
+
1179
+ if (this.fileUploadInitialized) {
1180
+ this._destroyFileUpload();
1181
+ }
1182
+ if (this.options.fileUpload) {
1183
+ this._initFileUpload();
1184
+ }
1185
+
886
1186
  this._applyOptions();
887
1187
  this.updatePreview();
888
1188
  }
889
1189
 
1190
+ showToolbar() {
1191
+ if (this.toolbar) {
1192
+ this.toolbar.show();
1193
+ } else {
1194
+ this._createToolbar();
1195
+ }
1196
+ }
1197
+
1198
+ hideToolbar() {
1199
+ if (this.toolbar) {
1200
+ this.toolbar.hide();
1201
+ }
1202
+ }
1203
+
890
1204
  /**
891
1205
  * Set theme for this instance
892
1206
  * @param {string|Object} theme - Theme name or custom theme object
893
1207
  * @returns {this} Returns this for chaining
894
1208
  */
895
1209
  setTheme(theme) {
896
- // Update instance theme
1210
+ OverType._autoInstances.delete(this);
897
1211
  this.instanceTheme = theme;
898
1212
 
899
- // Get theme object
900
- const themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
901
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1213
+ if (theme === 'auto') {
1214
+ OverType._autoInstances.add(this);
1215
+ OverType._startAutoListener();
1216
+ this._applyResolvedTheme(resolveAutoTheme('auto'));
1217
+ } else {
1218
+ const themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
1219
+ const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
902
1220
 
903
- // Update container theme attribute
904
- if (themeName) {
905
- this.container.setAttribute('data-theme', themeName);
1221
+ if (themeName) {
1222
+ this.container.setAttribute('data-theme', themeName);
1223
+ }
1224
+
1225
+ if (themeObj && themeObj.colors) {
1226
+ const cssVars = themeToCSSVars(themeObj.colors);
1227
+ this.container.style.cssText += cssVars;
1228
+ }
1229
+
1230
+ this.updatePreview();
906
1231
  }
907
1232
 
908
- // Apply CSS variables to container for instance override
1233
+ OverType._stopAutoListener();
1234
+ return this;
1235
+ }
1236
+
1237
+ _applyResolvedTheme(themeName) {
1238
+ const themeObj = getTheme(themeName);
1239
+ this.container.setAttribute('data-theme', themeName);
1240
+
909
1241
  if (themeObj && themeObj.colors) {
910
- const cssVars = themeToCSSVars(themeObj.colors);
911
- this.container.style.cssText += cssVars;
1242
+ this.container.style.cssText = themeToCSSVars(themeObj.colors);
912
1243
  }
913
1244
 
914
- // Update preview to reflect new theme
915
1245
  this.updatePreview();
916
-
917
- return this;
918
1246
  }
919
1247
 
920
1248
  /**
@@ -1122,6 +1450,13 @@ class OverType {
1122
1450
  * Destroy the editor instance
1123
1451
  */
1124
1452
  destroy() {
1453
+ OverType._autoInstances.delete(this);
1454
+ OverType._stopAutoListener();
1455
+
1456
+ if (this.fileUploadInitialized) {
1457
+ this._destroyFileUpload();
1458
+ }
1459
+
1125
1460
  // Remove instance reference
1126
1461
  this.element.overTypeInstance = null;
1127
1462
  OverType.instances.delete(this.element);
@@ -1178,7 +1513,7 @@ class OverType {
1178
1513
  }
1179
1514
  }
1180
1515
 
1181
- return new OverType(el, options);
1516
+ return new OverType(el, options)[0];
1182
1517
  });
1183
1518
  }
1184
1519
 
@@ -1246,59 +1581,89 @@ class OverType {
1246
1581
  * @param {Object} customColors - Optional color overrides
1247
1582
  */
1248
1583
  static setTheme(theme, customColors = null) {
1249
- // Process theme
1584
+ OverType._globalAutoTheme = false;
1585
+ OverType._globalAutoCustomColors = null;
1586
+
1587
+ if (theme === 'auto') {
1588
+ OverType._globalAutoTheme = true;
1589
+ OverType._globalAutoCustomColors = customColors;
1590
+ OverType._startAutoListener();
1591
+ OverType._applyGlobalTheme(resolveAutoTheme('auto'), customColors);
1592
+ return;
1593
+ }
1594
+
1595
+ OverType._stopAutoListener();
1596
+ OverType._applyGlobalTheme(theme, customColors);
1597
+ }
1598
+
1599
+ static _applyGlobalTheme(theme, customColors = null) {
1250
1600
  let themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
1251
1601
 
1252
- // Apply custom colors if provided
1253
1602
  if (customColors) {
1254
1603
  themeObj = mergeTheme(themeObj, customColors);
1255
1604
  }
1256
1605
 
1257
- // Store as current theme
1258
1606
  OverType.currentTheme = themeObj;
1259
-
1260
- // Re-inject styles with new theme
1261
1607
  OverType.injectStyles(true);
1262
1608
 
1263
- // Update all existing instances - update container theme attribute
1609
+ const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1610
+
1264
1611
  document.querySelectorAll('.overtype-container').forEach(container => {
1265
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1266
1612
  if (themeName) {
1267
1613
  container.setAttribute('data-theme', themeName);
1268
1614
  }
1269
1615
  });
1270
1616
 
1271
- // Also handle any old-style wrappers without containers
1272
1617
  document.querySelectorAll('.overtype-wrapper').forEach(wrapper => {
1273
1618
  if (!wrapper.closest('.overtype-container')) {
1274
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1275
1619
  if (themeName) {
1276
1620
  wrapper.setAttribute('data-theme', themeName);
1277
1621
  }
1278
1622
  }
1279
1623
 
1280
- // Trigger preview update for the instance
1281
1624
  const instance = wrapper._instance;
1282
1625
  if (instance) {
1283
1626
  instance.updatePreview();
1284
1627
  }
1285
1628
  });
1286
1629
 
1287
- // Update web components (shadow DOM instances)
1288
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1289
1630
  document.querySelectorAll('overtype-editor').forEach(webComponent => {
1290
- // Set the theme attribute to update the theme name
1291
1631
  if (themeName && typeof webComponent.setAttribute === 'function') {
1292
1632
  webComponent.setAttribute('theme', themeName);
1293
1633
  }
1294
- // Also call refreshTheme() to handle cases where the theme name stays the same
1295
- // but the theme object's properties have changed
1296
1634
  if (typeof webComponent.refreshTheme === 'function') {
1297
1635
  webComponent.refreshTheme();
1298
1636
  }
1299
1637
  });
1300
1638
  }
1301
1639
 
1640
+ static _startAutoListener() {
1641
+ if (OverType._autoMediaQuery) return;
1642
+ if (!window.matchMedia) return;
1643
+
1644
+ OverType._autoMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1645
+ OverType._autoMediaListener = (e) => {
1646
+ const resolved = e.matches ? 'cave' : 'solar';
1647
+
1648
+ if (OverType._globalAutoTheme) {
1649
+ OverType._applyGlobalTheme(resolved, OverType._globalAutoCustomColors);
1650
+ }
1651
+
1652
+ OverType._autoInstances.forEach(inst => inst._applyResolvedTheme(resolved));
1653
+ };
1654
+
1655
+ OverType._autoMediaQuery.addEventListener('change', OverType._autoMediaListener);
1656
+ }
1657
+
1658
+ static _stopAutoListener() {
1659
+ if (OverType._autoInstances.size > 0 || OverType._globalAutoTheme) return;
1660
+ if (!OverType._autoMediaQuery) return;
1661
+
1662
+ OverType._autoMediaQuery.removeEventListener('change', OverType._autoMediaListener);
1663
+ OverType._autoMediaQuery = null;
1664
+ OverType._autoMediaListener = null;
1665
+ }
1666
+
1302
1667
  /**
1303
1668
  * Set global code highlighter for all OverType instances
1304
1669
  * @param {Function|null} highlighter - Function that takes (code, language) and returns highlighted HTML
@@ -1428,4 +1793,4 @@ export default OverType;
1428
1793
  export { OverType };
1429
1794
 
1430
1795
  // Export toolbar buttons for custom toolbar configurations
1431
- export { toolbarButtons, defaultToolbarButtons } from './toolbar-buttons.js';
1796
+ export { toolbarButtons, defaultToolbarButtons } from './toolbar-buttons.js';
package/src/parser.js CHANGED
@@ -87,6 +87,7 @@ export class MarkdownParser {
87
87
  static parseHeader(html) {
88
88
  return html.replace(/^(#{1,3})\s(.+)$/, (match, hashes, content) => {
89
89
  const level = hashes.length;
90
+ content = this.parseInlineElements(content);
90
91
  return `<h${level}><span class="syntax-marker">${hashes} </span>${content}</h${level}>`;
91
92
  });
92
93
  }
@@ -121,6 +122,7 @@ export class MarkdownParser {
121
122
  */
122
123
  static parseBulletList(html) {
123
124
  return html.replace(/^((?:&nbsp;)*)([-*+])\s(.+)$/, (match, indent, marker, content) => {
125
+ content = this.parseInlineElements(content);
124
126
  return `${indent}<li class="bullet-list"><span class="syntax-marker">${marker} </span>${content}</li>`;
125
127
  });
126
128
  }
@@ -133,6 +135,7 @@ export class MarkdownParser {
133
135
  */
134
136
  static parseTaskList(html, isPreviewMode = false) {
135
137
  return html.replace(/^((?:&nbsp;)*)-\s+\[([ xX])\]\s+(.+)$/, (match, indent, checked, content) => {
138
+ content = this.parseInlineElements(content);
136
139
  if (isPreviewMode) {
137
140
  // Preview mode: render actual checkbox
138
141
  const isChecked = checked.toLowerCase() === 'x';
@@ -151,6 +154,7 @@ export class MarkdownParser {
151
154
  */
152
155
  static parseNumberedList(html) {
153
156
  return html.replace(/^((?:&nbsp;)*)(\d+\.)\s(.+)$/, (match, indent, marker, content) => {
157
+ content = this.parseInlineElements(content);
154
158
  return `${indent}<li class="ordered-list"><span class="syntax-marker">${marker} </span>${content}</li>`;
155
159
  });
156
160
  }
@@ -188,7 +192,7 @@ export class MarkdownParser {
188
192
  */
189
193
  static parseItalic(html) {
190
194
  // Single asterisk - must not be adjacent to other asterisks
191
- // Also must not be inside a syntax-marker span (to avoid matching bullet list markers)
195
+ // Must not be inside a syntax-marker span (avoid matching bullet list markers like ">* ")
192
196
  html = html.replace(/(?<![\*>])\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em><span class="syntax-marker">*</span>$1<span class="syntax-marker">*</span></em>');
193
197
 
194
198
  // Single underscore - must be at word boundaries to avoid matching inside words
@@ -464,8 +468,10 @@ export class MarkdownParser {
464
468
  html = this.parseBulletList(html);
465
469
  html = this.parseNumberedList(html);
466
470
 
467
- // Parse inline elements
468
- html = this.parseInlineElements(html);
471
+ // Parse inline elements (skip for headers and list items — already parsed inside those functions)
472
+ if (!html.includes('<li') && !html.includes('<h')) {
473
+ html = this.parseInlineElements(html);
474
+ }
469
475
 
470
476
  // Wrap in div to maintain line structure
471
477
  if (html.trim() === '') {