autumnnote 1.1.1 → 1.2.1

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.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "A modern, lightweight WYSIWYG editor — built with vanilla JavaScript, no jQuery required.",
5
5
  "main": "dist/autumnnote.umd.js",
6
6
  "module": "dist/autumnnote.es.js",
@@ -7,12 +7,18 @@
7
7
  * controlled by `bubbleToolbarItems` (array of button name strings).
8
8
  *
9
9
  * Built-in names: 'bold', 'italic', 'underline', 'strikethrough',
10
- * 'link', 'foreColor', 'removeFormat', 'inlineCode'.
10
+ * 'link', 'foreColor', 'hiliteColor', 'removeFormat', 'inlineCode'.
11
11
  * Any name not found in the built-in map is silently ignored.
12
12
  */
13
13
 
14
14
  import { on } from '../core/dom.js';
15
15
 
16
+ const COLOR_PRESETS = [
17
+ '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
18
+ '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
19
+ '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
20
+ ];
21
+
16
22
  // Minimal SVG icon set — only what the bubble toolbar needs.
17
23
  const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
18
24
  const _svg = (p) =>
@@ -25,21 +31,26 @@ const _ICONS = {
25
31
  strikethrough: _svg('<path d="M17.3 12H6.7"/><path d="M10 6.5C10 5.1 11.1 4 12.5 4c1.4 0 2.5 1.1 2.5 2.5 0 .8-.4 1.5-1 2"/><path d="M14 17.5C14 19 12.9 20 11.5 20 10.1 20 9 18.9 9 17.5c0-.8.4-1.5 1-2"/>'),
26
32
  link: _svg('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'),
27
33
  foreColor: _svg('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>'),
34
+ hiliteColor: _svg('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/><line x1="3" y1="21" x2="21" y2="21"/>'),
28
35
  removeFormat: _svg('<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/>'),
29
36
  inlineCode: _svg('<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 3h1a2 2 0 0 1 2 2v5c0 1.1.9 2 2 2a2 2 0 0 1-2 2v5a2 2 0 0 1-2 2h-1"/>'),
30
37
  };
31
38
 
39
+ // Color buttons are handled via _openColorPicker; not in _ACTIONS.
32
40
  const _ACTIONS = {
33
41
  bold: (ctx) => ctx.invoke('editor.bold'),
34
42
  italic: (ctx) => ctx.invoke('editor.italic'),
35
43
  underline: (ctx) => ctx.invoke('editor.underline'),
36
44
  strikethrough: (ctx) => ctx.invoke('editor.strikethrough'),
37
45
  link: (ctx) => ctx.invoke('linkDialog.show'),
38
- foreColor: (ctx) => {
39
- const color = window.prompt('Text color (hex or name):', '#000000');
40
- if (color) ctx.invoke('editor.foreColor', color);
46
+ removeFormat: (ctx) => {
47
+ // editor.removeFormat does not exist call execCommand directly
48
+ const editable = ctx.layoutInfo && ctx.layoutInfo.editable;
49
+ if (!editable) return;
50
+ editable.focus();
51
+ document.execCommand('removeFormat');
52
+ ctx.invoke('editor.afterCommand');
41
53
  },
42
- removeFormat: (ctx) => ctx.invoke('editor.removeFormat'),
43
54
  inlineCode: (ctx) => ctx.invoke('editor.inlineCode'),
44
55
  };
45
56
 
@@ -50,6 +61,8 @@ const _ACTIVE = {
50
61
  strikethrough: () => document.queryCommandState('strikeThrough'),
51
62
  };
52
63
 
64
+ const _COLOR_TYPES = { foreColor: true, hiliteColor: true };
65
+
53
66
  export class BubbleToolbar {
54
67
  /** @param {import('../Context.js').Context} context */
55
68
  constructor(context) {
@@ -58,6 +71,10 @@ export class BubbleToolbar {
58
71
 
59
72
  /** @type {HTMLElement|null} */
60
73
  this._el = null;
74
+ /** @type {HTMLElement|null} */
75
+ this._picker = null;
76
+ this._pickerType = null; // 'foreColor' | 'hiliteColor'
77
+ this._savedRange = null;
61
78
  this._visible = false;
62
79
  this._rafId = null;
63
80
  this._contextMenuOpen = false;
@@ -72,6 +89,7 @@ export class BubbleToolbar {
72
89
  initialize() {
73
90
  if (!this.options.bubbleToolbar) return this;
74
91
  this._build();
92
+ this._buildColorPicker();
75
93
  const d1 = on(document, 'selectionchange', this._onSelectionChange);
76
94
  const d2 = on(document, 'mousedown', this._onMousedown);
77
95
  const d3 = on(document, 'keydown', this._onKeydown);
@@ -90,6 +108,8 @@ export class BubbleToolbar {
90
108
  destroy() {
91
109
  if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
92
110
  this._el = null;
111
+ if (this._picker && this._picker.parentNode) this._picker.parentNode.removeChild(this._picker);
112
+ this._picker = null;
93
113
  this._disposers.forEach((d) => d());
94
114
  this._disposers = [];
95
115
  cancelAnimationFrame(this._rafId);
@@ -105,15 +125,30 @@ export class BubbleToolbar {
105
125
  el.setAttribute('role', 'toolbar');
106
126
  el.setAttribute('aria-label', 'Formatting');
107
127
 
108
- const items = this.options.bubbleToolbarItems || ['bold', 'italic', 'underline', 'link', 'foreColor', 'removeFormat'];
128
+ const items = this.options.bubbleToolbarItems || ['bold', 'italic', 'underline', 'link', 'foreColor', 'hiliteColor', 'removeFormat'];
109
129
  for (const name of items) {
110
- if (!_ICONS[name] || !_ACTIONS[name]) continue;
130
+ if (!_ICONS[name]) continue;
111
131
  const btn = document.createElement('button');
112
132
  btn.type = 'button';
113
- btn.className = 'an-bubble-btn';
114
133
  btn.dataset.name = name;
115
134
  btn.setAttribute('aria-label', name);
116
- btn.innerHTML = _ICONS[name];
135
+
136
+ if (_COLOR_TYPES[name]) {
137
+ // Color button: SVG icon stacked above a colored strip, matching ContextMenu style
138
+ btn.className = 'an-bubble-btn an-bubble-btn--color';
139
+ const iconWrap = document.createElement('span');
140
+ iconWrap.className = 'an-bubble-btn-svg';
141
+ iconWrap.innerHTML = _ICONS[name];
142
+ const strip = document.createElement('span');
143
+ strip.className = 'an-bubble-color-strip';
144
+ strip.style.background = name === 'foreColor' ? '#000000' : 'transparent';
145
+ btn.appendChild(iconWrap);
146
+ btn.appendChild(strip);
147
+ } else {
148
+ btn.className = 'an-bubble-btn';
149
+ btn.innerHTML = _ICONS[name];
150
+ }
151
+
117
152
  btn.addEventListener('mousedown', (e) => {
118
153
  // Prevent mousedown from collapsing selection before click fires
119
154
  e.preventDefault();
@@ -121,9 +156,15 @@ export class BubbleToolbar {
121
156
  btn.addEventListener('click', (e) => {
122
157
  e.preventDefault();
123
158
  e.stopPropagation();
159
+
160
+ // Color buttons: save selection and open the picker
161
+ if (_COLOR_TYPES[name]) {
162
+ this._openColorPicker(name, btn);
163
+ return;
164
+ }
165
+
124
166
  this.context.invoke('editor.focus');
125
- _ACTIONS[name](this.context);
126
- this.context.invoke('editor.afterCommand');
167
+ if (_ACTIONS[name]) _ACTIONS[name](this.context);
127
168
  this._syncActive();
128
169
  });
129
170
  el.appendChild(btn);
@@ -133,6 +174,138 @@ export class BubbleToolbar {
133
174
  this._el = el;
134
175
  }
135
176
 
177
+ _buildColorPicker() {
178
+ const picker = document.createElement('div');
179
+ picker.className = 'an-bubble-color-picker';
180
+ picker.style.display = 'none';
181
+
182
+ // Prevent any mousedown inside the picker from collapsing the editor selection
183
+ picker.addEventListener('mousedown', (e) => e.preventDefault());
184
+
185
+ const palette = document.createElement('div');
186
+ palette.className = 'an-context-color-palette';
187
+
188
+ COLOR_PRESETS.forEach((color) => {
189
+ const sw = document.createElement('div');
190
+ sw.className = 'an-context-color-swatch';
191
+ sw.title = color;
192
+ sw.style.background = color;
193
+ sw.setAttribute('role', 'button');
194
+ sw.setAttribute('aria-label', color);
195
+ sw.addEventListener('click', (e) => {
196
+ e.stopPropagation();
197
+ this._applyColor(this._pickerType, color);
198
+ });
199
+ palette.appendChild(sw);
200
+ });
201
+
202
+ // "No highlight" swatch — only shown for hiliteColor
203
+ const noColorBtn = document.createElement('div');
204
+ noColorBtn.className = 'an-context-color-swatch an-context-color-none';
205
+ noColorBtn.title = 'No highlight';
206
+ noColorBtn.setAttribute('role', 'button');
207
+ noColorBtn.setAttribute('aria-label', 'No highlight');
208
+ noColorBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="4" y1="4" x2="20" y2="20"/><line x1="20" y1="4" x2="4" y2="20"/></svg>`;
209
+ noColorBtn.addEventListener('click', (e) => {
210
+ e.stopPropagation();
211
+ this._applyColor('hiliteColor', 'transparent');
212
+ });
213
+
214
+ // Custom color input row
215
+ const customRow = document.createElement('div');
216
+ customRow.className = 'an-context-color-custom';
217
+ const colorInput = document.createElement('input');
218
+ colorInput.type = 'color';
219
+ colorInput.value = '#000000';
220
+ colorInput.title = 'Custom color';
221
+ // stopPropagation so the picker's mousedown-preventDefault doesn't block the native color dialog
222
+ colorInput.addEventListener('mousedown', (e) => e.stopPropagation());
223
+ colorInput.addEventListener('change', () => {
224
+ this._applyColor(this._pickerType, colorInput.value);
225
+ });
226
+ const customLabel = document.createElement('span');
227
+ customLabel.textContent = 'Custom…';
228
+ customRow.appendChild(colorInput);
229
+ customRow.appendChild(customLabel);
230
+
231
+ picker.appendChild(palette);
232
+ picker.appendChild(customRow);
233
+ document.body.appendChild(picker);
234
+
235
+ this._picker = picker;
236
+ this._picker._paletteEl = palette;
237
+ this._picker._noColorBtn = noColorBtn;
238
+ this._picker._colorInput = colorInput;
239
+ }
240
+
241
+ _openColorPicker(type, anchorBtn) {
242
+ // Save current selection before the picker might shift focus
243
+ const sel = window.getSelection();
244
+ if (sel && sel.rangeCount > 0) {
245
+ this._savedRange = sel.getRangeAt(0).cloneRange();
246
+ }
247
+
248
+ this._pickerType = type;
249
+
250
+ // Toggle "No highlight" swatch based on type
251
+ const palette = this._picker._paletteEl;
252
+ const noColorBtn = this._picker._noColorBtn;
253
+ if (type === 'hiliteColor') {
254
+ if (!palette.contains(noColorBtn)) palette.appendChild(noColorBtn);
255
+ } else {
256
+ if (palette.contains(noColorBtn)) palette.removeChild(noColorBtn);
257
+ }
258
+
259
+ // Seed the custom color input
260
+ this._picker._colorInput.value = type === 'foreColor' ? '#000000' : '#ffff00';
261
+
262
+ // Position the picker above the bubble toolbar (not blocking the content below it).
263
+ // Fall back to below the toolbar if there's not enough room above.
264
+ this._picker.style.display = 'block';
265
+ const pw = this._picker.offsetWidth;
266
+ const ph = this._picker.offsetHeight;
267
+ const toolbarRect = this._el.getBoundingClientRect();
268
+
269
+ let top = toolbarRect.top - ph - 6;
270
+ if (top < 8) top = toolbarRect.bottom + 6;
271
+
272
+ // Align horizontally with the clicked button, clamped to viewport
273
+ const btnRect = anchorBtn.getBoundingClientRect();
274
+ let left = btnRect.left;
275
+ left = Math.max(8, Math.min(left, window.innerWidth - pw - 8));
276
+
277
+ this._picker.style.left = `${left}px`;
278
+ this._picker.style.top = `${top}px`;
279
+ }
280
+
281
+ _closeColorPicker() {
282
+ if (this._picker) this._picker.style.display = 'none';
283
+ this._pickerType = null;
284
+ }
285
+
286
+ /** Restore the saved selection, apply execCommand, update the color strip, then close the picker. */
287
+ _applyColor(type, color) {
288
+ const editable = this.context.layoutInfo && this.context.layoutInfo.editable;
289
+ if (!editable || !this._savedRange) return;
290
+
291
+ editable.focus();
292
+ const sel = window.getSelection();
293
+ sel.removeAllRanges();
294
+ sel.addRange(this._savedRange.cloneRange());
295
+
296
+ document.execCommand(type, false, color);
297
+ this.context.invoke('editor.afterCommand');
298
+
299
+ // Update the color strip on the corresponding button
300
+ const name = type === 'hiliteColor' ? 'hiliteColor' : 'foreColor';
301
+ const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
302
+ const strip = btn && btn.querySelector('.an-bubble-color-strip');
303
+ if (strip) strip.style.background = color === 'transparent' ? 'transparent' : color;
304
+
305
+ this._closeColorPicker();
306
+ this._syncActive();
307
+ }
308
+
136
309
  // ---------------------------------------------------------------------------
137
310
  // Show / hide
138
311
  // ---------------------------------------------------------------------------
@@ -164,6 +337,7 @@ export class BubbleToolbar {
164
337
  el.style.visibility = '';
165
338
 
166
339
  this._syncActive();
340
+ this._syncColorStrips();
167
341
  this._visible = true;
168
342
  }
169
343
 
@@ -171,6 +345,7 @@ export class BubbleToolbar {
171
345
  if (!this._el) return;
172
346
  this._el.style.display = 'none';
173
347
  this._visible = false;
348
+ this._closeColorPicker();
174
349
  }
175
350
 
176
351
  _syncActive() {
@@ -182,6 +357,28 @@ export class BubbleToolbar {
182
357
  });
183
358
  }
184
359
 
360
+ /** Read the current selection's color and update the color-strip indicators. */
361
+ _syncColorStrips() {
362
+ if (!this._el) return;
363
+ const sel = window.getSelection();
364
+ if (!sel || !sel.rangeCount) return;
365
+ let node = sel.getRangeAt(0).startContainer;
366
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
367
+ if (!node) return;
368
+ const cs = window.getComputedStyle(node);
369
+
370
+ const foreBtn = this._el.querySelector('[data-name="foreColor"]');
371
+ const foreStrip = foreBtn && foreBtn.querySelector('.an-bubble-color-strip');
372
+ if (foreStrip) foreStrip.style.background = cs.color || '#000000';
373
+
374
+ const hiliteBtn = this._el.querySelector('[data-name="hiliteColor"]');
375
+ const hiliteStrip = hiliteBtn && hiliteBtn.querySelector('.an-bubble-color-strip');
376
+ if (hiliteStrip) {
377
+ const bg = cs.backgroundColor;
378
+ hiliteStrip.style.background = (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
379
+ }
380
+ }
381
+
185
382
  // ---------------------------------------------------------------------------
186
383
  // Events
187
384
  // ---------------------------------------------------------------------------
@@ -190,6 +387,8 @@ export class BubbleToolbar {
190
387
  cancelAnimationFrame(this._rafId);
191
388
  this._rafId = requestAnimationFrame(() => {
192
389
  if (this._contextMenuOpen) return;
390
+ // Keep toolbar visible while color picker is open
391
+ if (this._picker && this._picker.style.display !== 'none') return;
193
392
 
194
393
  const sel = window.getSelection();
195
394
  if (!sel || sel.isCollapsed || !sel.rangeCount) {
@@ -203,7 +402,7 @@ export class BubbleToolbar {
203
402
  return;
204
403
  }
205
404
 
206
- // Don't show inside code view or read-only
405
+ // Don't show in read-only mode
207
406
  if (this.context.options.readOnly) {
208
407
  this._hide();
209
408
  return;
@@ -221,17 +420,22 @@ export class BubbleToolbar {
221
420
  }
222
421
 
223
422
  _onMousedown(e) {
224
- // Hide when clicking outside both the editable and the bubble toolbar
423
+ // Hide when clicking outside both the editable, the bubble toolbar, and the color picker
225
424
  if (!this._visible) return;
226
425
  if (this._el && this._el.contains(e.target)) return;
426
+ if (this._picker && this._picker.contains(e.target)) return;
227
427
  const editable = this.context.layoutInfo.editable;
228
428
  if (editable.contains(e.target)) return;
229
429
  this._hide();
230
430
  }
231
431
 
232
432
  _onKeydown(e) {
233
- if (e.key === 'Escape' && this._visible) {
234
- this._hide();
433
+ if (e.key === 'Escape') {
434
+ if (this._picker && this._picker.style.display !== 'none') {
435
+ this._closeColorPicker();
436
+ } else if (this._visible) {
437
+ this._hide();
438
+ }
235
439
  }
236
440
  }
237
441
 
@@ -237,7 +237,23 @@ export class Toolbar {
237
237
 
238
238
  const openPopup = () => {
239
239
  isOpen = true;
240
+ const rect = btn.getBoundingClientRect();
241
+
242
+ // Measure popup dimensions while invisible so we can set the correct
243
+ // position before the browser paints (matches the color-picker pattern).
244
+ popup.style.visibility = 'hidden';
240
245
  popup.style.display = 'block';
246
+ const pw = popup.offsetWidth;
247
+ const ph = popup.offsetHeight;
248
+
249
+ let left = rect.left;
250
+ let top = rect.bottom + 4;
251
+ if (left + pw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - pw - 8);
252
+ if (top + ph > window.innerHeight - 8) top = rect.top - ph - 4;
253
+
254
+ popup.style.left = `${left}px`;
255
+ popup.style.top = `${top}px`;
256
+ popup.style.visibility = '';
241
257
  btn.setAttribute('aria-expanded', 'true');
242
258
  };
243
259
 
@@ -273,10 +289,14 @@ export class Toolbar {
273
289
 
274
290
  const d5 = on(document, 'click', () => { if (isOpen) closePopup(); });
275
291
 
276
- this._disposers.push(d1, d2, d3, d4, d5);
292
+ // Append popup to body so position:fixed is truly viewport-relative,
293
+ // unaffected by any ancestor transform / filter (same pattern as color picker).
294
+ this._disposers.push(d1, d2, d3, d4, d5, () => {
295
+ if (popup.parentNode) popup.parentNode.removeChild(popup);
296
+ });
277
297
 
278
298
  wrap.appendChild(btn);
279
- wrap.appendChild(popup);
299
+ document.body.appendChild(popup);
280
300
  return wrap;
281
301
  }
282
302
 
@@ -153,7 +153,7 @@ export const defaultOptions = {
153
153
 
154
154
  // Bubble toolbar: show a mini floating toolbar above text selections.
155
155
  bubbleToolbar: false,
156
- bubbleToolbarItems: ['bold', 'italic', 'underline', 'link', 'foreColor', 'removeFormat'],
156
+ bubbleToolbarItems: ['bold', 'italic', 'underline', 'link', 'foreColor', 'hiliteColor', 'removeFormat'],
157
157
 
158
158
  // @mention support. mention.onSearch(query, callback) must be provided to activate.
159
159
  // mention.minChars defaults to 0 — dropdown opens immediately on trigger character.
@@ -108,6 +108,8 @@
108
108
  background: $an-bg-toolbar;
109
109
  border-bottom: 1px solid $an-border;
110
110
  user-select: none;
111
+ // Round the top corners to match the container border-radius
112
+ border-radius: $an-radius $an-radius 0 0;
111
113
  }
112
114
 
113
115
  .an-btn-group {
@@ -193,9 +195,7 @@
193
195
 
194
196
  .an-table-picker-popup {
195
197
  display: none;
196
- position: absolute;
197
- top: calc(100% + 4px);
198
- left: 0;
198
+ position: fixed; // top/left set dynamically by JS — escapes overflow-clipping ancestors
199
199
  background: $an-bg;
200
200
  border: 1px solid $an-border;
201
201
  border-radius: 6px;
@@ -761,6 +761,13 @@
761
761
  font-size: 11px;
762
762
  color: $an-muted;
763
763
  user-select: none;
764
+ // Round the bottom corners to match the container border-radius
765
+ border-radius: 0 0 $an-radius $an-radius;
766
+ }
767
+
768
+ // When there is no statusbar, the editable area is the last child — round its bottom corners
769
+ .an-container > .an-editable:last-child {
770
+ border-radius: 0 0 $an-radius $an-radius;
764
771
  }
765
772
 
766
773
  .an-status-info {
@@ -1890,6 +1897,37 @@ $an-video-accent: #8b5cf6; // violet-500
1890
1897
  }
1891
1898
  }
1892
1899
 
1900
+ // =============================================================================
1901
+ // Responsive — mobile / small-viewport toolbar
1902
+ // =============================================================================
1903
+ //
1904
+ // On screens ≤ 640 px the default flex-wrap:wrap behaviour creates a multi-row
1905
+ // toolbar that pushes the editable area too far down. We switch automatically
1906
+ // to a single horizontally-scrollable row (same as toolbarOverflow:'scroll'),
1907
+ // hide the scrollbar track, and compact the dropdown selects.
1908
+
1909
+ @media (max-width: 640px) {
1910
+ .an-toolbar {
1911
+ flex-wrap: nowrap;
1912
+ overflow-x: auto;
1913
+ // Hide scrollbar visually — users swipe naturally on touch devices
1914
+ scrollbar-width: none;
1915
+ -ms-overflow-style: none;
1916
+
1917
+ &::-webkit-scrollbar { display: none; }
1918
+ }
1919
+
1920
+ // Compact dropdown selects to save horizontal real-estate on small screens
1921
+ .an-select {
1922
+ min-width: 72px;
1923
+ max-width: 90px;
1924
+ font-size: 11px;
1925
+ }
1926
+
1927
+ .an-select-style { min-width: 62px; max-width: 76px; }
1928
+ .an-select-narrow { min-width: 50px; max-width: 66px; }
1929
+ }
1930
+
1893
1931
  // =============================================================================
1894
1932
  // RTL support
1895
1933
  // =============================================================================
@@ -2117,6 +2155,30 @@ $an-video-accent: #8b5cf6; // violet-500
2117
2155
  color: $an-primary;
2118
2156
  }
2119
2157
 
2158
+ // Color variant: SVG stacked above a thin color-indicator strip
2159
+ &.an-bubble-btn--color {
2160
+ flex-direction: column;
2161
+ gap: 1px;
2162
+ padding: 4px 0 3px;
2163
+
2164
+ .an-bubble-btn-svg {
2165
+ display: flex;
2166
+ align-items: center;
2167
+ justify-content: center;
2168
+ line-height: 0;
2169
+
2170
+ svg { display: block; }
2171
+ }
2172
+
2173
+ .an-bubble-color-strip {
2174
+ display: block;
2175
+ width: 14px;
2176
+ height: 3px;
2177
+ border-radius: 1px;
2178
+ pointer-events: none;
2179
+ }
2180
+ }
2181
+
2120
2182
  .an-theme-dark & {
2121
2183
  color: #cdd6f4;
2122
2184
 
@@ -2131,6 +2193,23 @@ $an-video-accent: #8b5cf6; // violet-500
2131
2193
  }
2132
2194
  }
2133
2195
 
2196
+ // Bubble toolbar — color picker popup
2197
+ .an-bubble-color-picker {
2198
+ position: fixed;
2199
+ z-index: 10041;
2200
+ background: $an-bg;
2201
+ border: 1px solid $an-border;
2202
+ border-radius: 8px;
2203
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.14);
2204
+ padding: 4px 0;
2205
+ min-width: 160px;
2206
+
2207
+ .an-theme-dark & {
2208
+ background: #24273a;
2209
+ border-color: #3f3f5f;
2210
+ }
2211
+ }
2212
+
2134
2213
  // =============================================================================
2135
2214
  // @mention dropdown + chip (#5)
2136
2215
  // =============================================================================