@aquera/nile-elements 0.1.64-beta-1.2 → 0.1.65-beta-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 (38) hide show
  1. package/demo/index.html +29 -109
  2. package/dist/index.js +608 -547
  3. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js +1 -1
  4. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js.map +1 -1
  5. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
  6. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
  7. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +45 -2
  8. package/dist/nile-rich-text-editor/nile-rich-text-editor.esm.js +1 -1
  9. package/dist/nile-rich-text-editor/nile-rte-mentions.cjs.js +1 -1
  10. package/dist/nile-rich-text-editor/nile-rte-mentions.cjs.js.map +1 -1
  11. package/dist/nile-rich-text-editor/nile-rte-mentions.esm.js +1 -1
  12. package/dist/nile-rich-text-editor/nile-rte-select.cjs.js +1 -1
  13. package/dist/nile-rich-text-editor/nile-rte-select.cjs.js.map +1 -1
  14. package/dist/nile-rich-text-editor/nile-rte-select.esm.js +31 -13
  15. package/dist/nile-rich-text-editor/utils.cjs.js +1 -1
  16. package/dist/nile-rich-text-editor/utils.cjs.js.map +1 -1
  17. package/dist/nile-rich-text-editor/utils.esm.js +1 -1
  18. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +43 -0
  19. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
  20. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.d.ts +3 -0
  21. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js +81 -22
  22. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js.map +1 -1
  23. package/dist/src/nile-rich-text-editor/nile-rte-mentions.js +5 -0
  24. package/dist/src/nile-rich-text-editor/nile-rte-mentions.js.map +1 -1
  25. package/dist/src/nile-rich-text-editor/nile-rte-select.d.ts +15 -1
  26. package/dist/src/nile-rich-text-editor/nile-rte-select.js +85 -52
  27. package/dist/src/nile-rich-text-editor/nile-rte-select.js.map +1 -1
  28. package/dist/src/nile-rich-text-editor/utils.d.ts +1 -0
  29. package/dist/src/nile-rich-text-editor/utils.js +17 -0
  30. package/dist/src/nile-rich-text-editor/utils.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +43 -0
  34. package/src/nile-rich-text-editor/nile-rich-text-editor.ts +112 -40
  35. package/src/nile-rich-text-editor/nile-rte-mentions.ts +5 -0
  36. package/src/nile-rich-text-editor/nile-rte-select.ts +97 -58
  37. package/src/nile-rich-text-editor/utils.ts +18 -0
  38. package/vscode-html-custom-data.json +3 -3
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent nile-elements following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "nile-elements",
6
- "version": "0.1.64-beta-1.2",
6
+ "version": "0.1.65-beta-1.0",
7
7
  "main": "dist/src/index.js",
8
8
  "type": "module",
9
9
  "module": "dist/src/index.js",
@@ -44,6 +44,49 @@ nile-rte-toolbar-item > nile-button::part(base) {
44
44
 
45
45
  .editor { min-height:160px; padding:12px; border:1px solid #e5e7eb; border-radius:0 0 8px 8px; background:#fff; outline:none; }
46
46
  nile-rte-preview { display:block; margin-top:10px; padding:10px; border:1px dashed #cbd5e1; border-radius:8px; background:#fafafa; }
47
+
48
+ .rte-color-trigger {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ height: 28px;
53
+ padding: 0 8px;
54
+ border: 1px solid var(--nile-color-border, #d9d9d9);
55
+ border-radius: 6px;
56
+ background: #fff;
57
+ cursor: pointer;
58
+ }
59
+
60
+ .rte-color-trigger .glyph-stack {
61
+ display: grid; /* stack vertically */
62
+ grid-auto-rows: max-content;
63
+ align-items: center;
64
+ justify-items: center;
65
+ line-height: 1;
66
+ }
67
+
68
+ .rte-color-trigger .glyph {
69
+ font-size: 14px;
70
+ line-height: 1;
71
+ margin-bottom: 2px; /* little breathing space above underline */
72
+ }
73
+
74
+ .rte-color-trigger .underline {
75
+ width: 18px;
76
+ height: 3px;
77
+ border-radius: 2px;
78
+ background: currentColor; /* we override via JS with backgroundColor */
79
+ }
80
+
81
+ /* (unchanged) highlight square */
82
+ .rte-color-trigger .swatch-box {
83
+ width: 18px;
84
+ height: 16px;
85
+ border-radius: 4px;
86
+ border: 1px solid rgba(0,0,0,0.35);
87
+ background: currentColor; /* overridden via JS */
88
+ }
89
+
47
90
  `;
48
91
 
49
92
  export default [styles];
@@ -13,7 +13,7 @@ import './nile-rte-mentions';
13
13
  import {
14
14
  closestBlock, nearestElement, rgbToHex,
15
15
  toggleInlineTag, setBlockTag, setAlignment,
16
- setFontFamily, setForeColor, insertOrEditLink
16
+ setFontFamily, setForeColor, insertOrEditLink, setBackColor
17
17
  } from './utils';
18
18
 
19
19
  import {styles} from './nile-rich-text-editor.css';
@@ -83,6 +83,10 @@ export class NileRichTextEditor extends LitElement {
83
83
  private headingSelect: HTMLSelectElement | null = null;
84
84
  private fontSelect: HTMLSelectElement | null = null;
85
85
  private colorInput: HTMLInputElement | null = null;
86
+ private bgColorInput: HTMLInputElement | null = null;
87
+ private colorSwatchEl: HTMLElement | null = null;
88
+ private bgSwatchEl: HTMLElement | null = null;
89
+
86
90
 
87
91
  // Mentions controller (child)
88
92
  private mentionsEl: HTMLElement | null = null;
@@ -100,27 +104,27 @@ export class NileRichTextEditor extends LitElement {
100
104
  connectedCallback(): void {
101
105
  super.connectedCallback();
102
106
 
103
- // 1) Inject styles once (so authored DOM stays as-is)
107
+
104
108
  this.injectCss(styles.cssText);
105
109
 
106
110
 
107
- // 2) Keep authored nodes
111
+
108
112
  this.toolbarEl = this.querySelector('nile-rte-toolbar');
109
113
  this.previewEl = this.querySelector('nile-rte-preview');
110
114
 
111
- // 3) Ensure there is an editor element; create if missing.
115
+
112
116
  this.ensureEditor();
113
117
 
114
- // 4) Fill initial content
118
+
115
119
  if (this.value && !this.editorEl.innerHTML.trim()) {
116
120
  this.editorEl.innerHTML = this.value;
117
121
  }
118
122
  this.content = this.editorEl.innerHTML;
119
123
 
120
- // 5) Wire toolbar authored children directly
124
+
121
125
  if (this.toolbarEl) this.wireAuthoredToolbar(this.toolbarEl);
122
126
 
123
- // 6) Mentions: delegate to child controller
127
+
124
128
  this.mentionsEl = this.querySelector('nile-rte-mentions');
125
129
  if (this.mentionsEl) {
126
130
 
@@ -142,7 +146,7 @@ export class NileRichTextEditor extends LitElement {
142
146
 
143
147
  disconnectedCallback(): void {
144
148
  document.removeEventListener('selectionchange', this.onSelectionChange, true);
145
- // Optionally tell mentions to cleanup
149
+
146
150
  if (this.mentionsEl && (this.mentionsEl as any).detach) {
147
151
  (this.mentionsEl as any).detach();
148
152
  }
@@ -167,7 +171,6 @@ export class NileRichTextEditor extends LitElement {
167
171
  }
168
172
  this.editorEl = editor;
169
173
  }
170
- // **NEW**: seed an empty paragraph so setBlockTag() has something to swap
171
174
  if (!this.editorEl.innerHTML.trim()) {
172
175
  this.editorEl.innerHTML = '<p><br></p>';
173
176
  }
@@ -181,17 +184,17 @@ export class NileRichTextEditor extends LitElement {
181
184
  this.updateContent();
182
185
  });
183
186
  this.editorEl.addEventListener('mouseup', () => this.saveSelection());
184
- this.editorEl.addEventListener('keyup', () => this.saveSelection()); // keep range fresh for toolbar
187
+ this.editorEl.addEventListener('keyup', () => this.saveSelection());
185
188
  }
186
189
 
187
190
  private wireAuthoredToolbar(tb: HTMLElement) {
188
- // Clear previous map
191
+
189
192
  this.buttonMap.clear();
190
193
  this.headingSelect = null;
191
194
  this.fontSelect = null;
192
195
  this.colorInput = null;
193
196
 
194
- // Process children
197
+
195
198
  Array.from(tb.children).forEach(child => {
196
199
  const tag = child.tagName.toLowerCase();
197
200
 
@@ -208,7 +211,7 @@ export class NileRichTextEditor extends LitElement {
208
211
 
209
212
 
210
213
  if (tag === 'nile-rte-toolbar-item') {
211
- // Look for an authored <nile-button> (so authors can override)
214
+
212
215
  let btn = child.querySelector(':scope > nile-button') as HTMLElement | null;
213
216
 
214
217
  const cmd = child.getAttribute('name') || '';
@@ -216,14 +219,14 @@ export class NileRichTextEditor extends LitElement {
216
219
  const iconAttr = child.getAttribute('icon');
217
220
  const authoredHasContent = child.innerHTML.trim().length > 0;
218
221
 
219
- // If none, create one
222
+
220
223
  if (!btn) {
221
224
  btn = document.createElement('nile-button');
222
225
  (btn as any).variant = 'tertiary';
223
226
  (btn as any).size = 'small'; // optional
224
227
  }
225
228
 
226
- // Fill visual content
229
+
227
230
  if (iconAttr) {
228
231
  btn.innerHTML = `<nile-icon name="${iconAttr}" aria-label="${label}"></nile-icon>`;
229
232
  child.innerHTML = '';
@@ -284,32 +287,89 @@ export class NileRichTextEditor extends LitElement {
284
287
  }
285
288
 
286
289
 
287
- if (tag === 'nile-rte-color') {
288
- const label = child.getAttribute('label') ?? 'Text color';
289
- const value = child.getAttribute('value') ?? '#000000';
290
- // render internal <input type="color">
291
- let input = child.querySelector(':scope > input[type="color"]') as HTMLInputElement | null;
292
- if (!input) {
293
- input = document.createElement('input');
294
- input.type = 'color';
295
- child.appendChild(input);
296
- }
297
- input.title = label;
298
- input.value = value;
299
- input.addEventListener('mousedown', e => e.preventDefault());
300
- input.addEventListener('input', () => {
301
- this.focusAndRestore();
302
- setForeColor(this.editorEl, input!.value);
303
- this.updateContent(); this.updateToolbarState();
304
- });
305
- this.colorInput = input;
306
- }
307
-
308
- // nile-rte-divider: no-op (purely visual)
290
+ if (tag === 'nile-rte-color') {
291
+ const label = child.getAttribute('label') ?? 'Text color';
292
+ const value = child.getAttribute('value') ?? '#000000';
293
+ const mode = child.getAttribute('mode') ?? 'text'; // 'text' | 'background'
294
+
295
+ // Create/attach the hidden color input
296
+ let input = child.querySelector(':scope > input[type="color"]') as HTMLInputElement | null;
297
+ if (!input) {
298
+ input = document.createElement('input');
299
+ input.type = 'color';
300
+ input.style.position = 'absolute';
301
+ input.style.opacity = '0';
302
+ input.style.pointerEvents = 'none'; // we'll click it programmatically
303
+ child.appendChild(input);
304
+ }
305
+ input.title = label;
306
+ input.value = value;
307
+
308
+ // Build a custom trigger that shows the current color
309
+ let trigger = child.querySelector(':scope > button.rte-color-trigger') as HTMLButtonElement | null;
310
+ if (!trigger) {
311
+ trigger = document.createElement('button');
312
+ trigger.type = 'button';
313
+ trigger.className = 'rte-color-trigger';
314
+ trigger.setAttribute('aria-label', label);
315
+
316
+ if (mode === 'background') {
317
+ trigger.innerHTML = `
318
+ <span class="swatch-box" aria-hidden="true"></span>
319
+ `;
320
+ } else {
321
+ trigger.innerHTML = `
322
+ <span class="glyph-stack" aria-hidden="true">
323
+ <span class="glyph">A</span>
324
+ <span class="underline"></span>
325
+ </span>
326
+ `;
327
+ }
328
+ child.appendChild(trigger);
329
+ }
330
+
331
+ // Cache swatch elements to update later
332
+ const underline = trigger.querySelector('.underline') as HTMLElement | null;
333
+ const square = trigger.querySelector('.swatch-box') as HTMLElement | null;
334
+
335
+ if (mode === 'background') {
336
+ this.bgColorInput = input;
337
+ this.bgSwatchEl = square;
338
+ if (this.bgSwatchEl) this.bgSwatchEl.style.backgroundColor = input.value;
339
+ } else {
340
+ this.colorInput = input;
341
+ this.colorSwatchEl = underline;
342
+ if (this.colorSwatchEl) this.colorSwatchEl.style.backgroundColor = input.value;
343
+ }
344
+
345
+ // Open native picker on trigger click
346
+ trigger.addEventListener('click', (e) => {
347
+ e.preventDefault();
348
+ // Keep selection before opening the picker
349
+ this.focusAndRestore();
350
+ input!.click();
351
+ });
352
+
353
+ // When the user picks a color, apply it + update swatch
354
+ input.addEventListener('input', () => {
355
+ this.focusAndRestore();
356
+ if (mode === 'background') {
357
+ setBackColor(this.editorEl, input!.value);
358
+ if (this.bgSwatchEl) this.bgSwatchEl.style.backgroundColor = input!.value;
359
+ } else {
360
+ setForeColor(this.editorEl, input!.value);
361
+ if (this.colorSwatchEl) this.colorSwatchEl.style.backgroundColor = input!.value;
362
+ }
363
+ this.updateContent();
364
+ this.updateToolbarState();
365
+ });
366
+
367
+ // Prevent losing selection when interacting
368
+ trigger.addEventListener('mousedown', e => e.preventDefault());
369
+ input.addEventListener('mousedown', e => e.preventDefault());
370
+ }
309
371
  });
310
372
  }
311
-
312
- // ---------- Selection helpers (for toolbar state only) ----------
313
373
  private onSelectionChange = () => {
314
374
  if (!this.editorEl) return;
315
375
  const sel = document.getSelection();
@@ -533,13 +593,25 @@ private ensureAtLeastOneParagraph() {
533
593
  }
534
594
  }
535
595
 
536
- // Reflect color
537
596
  if (this.colorInput) {
538
597
  const hex = rgbToHex(comp.color);
539
598
  if (hex && this.colorInput.value.toLowerCase() !== hex.toLowerCase()) {
540
599
  this.colorInput.value = hex;
541
600
  }
601
+ if (this.colorSwatchEl) this.colorSwatchEl.style.backgroundColor = this.colorInput.value;
542
602
  }
603
+
604
+
605
+ if (this.bgColorInput) {
606
+ const bg = getComputedStyle(startElm).backgroundColor;
607
+ if (bg && !/transparent|rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/i.test(bg)) {
608
+ const bgHex = rgbToHex(bg);
609
+ if (bgHex && this.bgColorInput.value.toLowerCase() !== bgHex.toLowerCase()) {
610
+ this.bgColorInput.value = bgHex;
611
+ }
612
+ }
613
+ if (this.bgSwatchEl) this.bgSwatchEl.style.backgroundColor = this.bgColorInput.value;
614
+ }
543
615
  }
544
616
 
545
617
  private syncPreview() {
@@ -186,6 +186,10 @@ private triggerBtn: HTMLElement | null = null;
186
186
  .mention-dropdown li { padding: 6px 8px; cursor: pointer; border-radius: 4px; }
187
187
  .mention-dropdown li:hover { background: #f1f5f9; }
188
188
  .mention { background: #eef2ff; padding: 0 3px; border-radius: 3px; }
189
+ nile-menu.mentions-menu::part(menu__items-wrapper){
190
+ max-height: 260px;
191
+ }
192
+
189
193
  `;
190
194
  this.insertBefore(style, this.firstChild);
191
195
  }
@@ -214,6 +218,7 @@ private ensureMentionDropdown() {
214
218
  dd.appendChild(btn);
215
219
  this.triggerBtn = btn;
216
220
  const menu = document.createElement('nile-menu');
221
+ menu.classList.add('mentions-menu');
217
222
  dd.appendChild(menu);
218
223
 
219
224
  this.hostEl.appendChild(dd);
@@ -2,6 +2,18 @@
2
2
  import { LitElement, html } from 'lit';
3
3
  import { customElement, property, state } from 'lit/decorators.js';
4
4
 
5
+ type HeadingTag = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
+ type GenericOption = { value: string; label?: string; icon?: string };
7
+ type HeadingOption = { value: HeadingTag; label?: string; icon?: string };
8
+ type NormalizedOption = { value: string; label: string; icon?: string };
9
+
10
+ const HEADING_ALLOWLIST: ReadonlySet<HeadingTag> = new Set([
11
+ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
12
+ ]);
13
+
14
+ function isHeadingTag(v: string): v is HeadingTag {
15
+ return HEADING_ALLOWLIST.has(v as HeadingTag);
16
+ }
5
17
 
6
18
  @customElement('nile-rte-select')
7
19
  export class NileRteSelect extends LitElement {
@@ -9,8 +21,14 @@ export class NileRteSelect extends LitElement {
9
21
 
10
22
  /** 'heading' | 'font' | 'align' */
11
23
  @property({ type: String }) type = '';
12
- /** JSON: [{ value, label?, icon? }, ...] */
24
+
25
+ /** JSON: [{ value, label?, icon? }, ...] (attribute-based; runtime-validated) */
13
26
  @property({ type: String }) options = '[]';
27
+
28
+ /** Programmatic options (preferred for TS safety). */
29
+ @property({ attribute: false })
30
+ optionsObj?: Array<GenericOption | HeadingOption>;
31
+
14
32
  /** Fallback label for trigger (e.g., "Align") */
15
33
  @property({ type: String }) label = '';
16
34
 
@@ -26,18 +44,37 @@ export class NileRteSelect extends LitElement {
26
44
  return map[v] || 'align-left';
27
45
  }
28
46
 
29
- private get parsedOptions(): Array<{ value: string; label: string; icon?: string }> {
30
- try {
31
- const raw = JSON.parse(this.options);
32
- return raw.map((o: any) => {
33
- const value = o.value ?? o;
34
- const label = o.label ?? o.value ?? o;
35
- const icon = o.icon ?? (this.type === 'align' ? this.mapAlignIcon(String(value)) : undefined);
36
- return { value, label, icon };
37
- });
38
- } catch {
39
- return [];
47
+ private get parsedOptions(): NormalizedOption[] {
48
+ // Prefer programmatic options if present (gives TS compile-time checks)
49
+ const source: unknown = this.optionsObj ?? this.options;
50
+
51
+ const rawArray: any[] = (() => {
52
+ if (Array.isArray(source)) return source;
53
+ try { return JSON.parse(String(source)); } catch { return []; }
54
+ })();
55
+
56
+ // Normalize to consistent shape
57
+ let items: NormalizedOption[] = rawArray.map((o: any) => {
58
+ const value: string = o?.value ?? o;
59
+ const label: string = o?.label ?? o?.value ?? o;
60
+ const icon: string | undefined =
61
+ o?.icon ?? (this.type === 'align' ? this.mapAlignIcon(String(value)) : undefined);
62
+ return { value, label, icon };
63
+ });
64
+
65
+ // If type is heading, enforce allowlist (runtime validation)
66
+ if (this.type === 'heading') {
67
+ const before = items.length;
68
+ items = items.filter(i => isHeadingTag(i.value));
69
+ if (items.length !== before) {
70
+ }
71
+ // If current selection is invalid for heading, reset
72
+ if (this.selectedValue && !isHeadingTag(this.selectedValue)) {
73
+ this.selectedValue = '';
74
+ }
40
75
  }
76
+
77
+ return items;
41
78
  }
42
79
 
43
80
  private ensureDefault() {
@@ -48,57 +85,56 @@ export class NileRteSelect extends LitElement {
48
85
  }
49
86
 
50
87
  private onSelect(value: string) {
88
+ if (this.type === 'heading' && !isHeadingTag(value)) {
89
+ console.warn(`[nile-rte-select] Ignoring invalid heading value: ${value}`);
90
+ return;
91
+ }
51
92
  this.selectedValue = value;
52
93
  this.dispatchEvent(new CustomEvent('change', {
53
94
  detail: value, bubbles: true, composed: true
54
95
  }));
55
96
  }
56
97
 
57
-
58
-
59
98
  connectedCallback(): void {
60
99
  super.connectedCallback();
61
- this.injectLocalStyles();
100
+ this.injectLocalStyles();
62
101
  }
63
102
 
64
103
  private injectLocalStyles() {
65
-
66
104
  if (this.querySelector('style[data-rte-select-style]')) return;
67
105
 
68
106
  const style = document.createElement('style');
69
107
  style.setAttribute('data-rte-select-style', 'true');
70
108
  style.textContent = `
71
-
72
- nile-menu.rte-align-menu::part(menu__items-wrapper) {
73
- display: flex;
74
- }
75
- nile-menu.rte-align-menu,nile-menu.rte-default-menu{
76
- margin-top: 0px;
77
- }
78
-
79
- nile-button.rte-align-trigger::part(base),nile-button.rte-default-trigger::part(base){
80
- min-width: 32px;
81
- height: 32px;
82
- padding: 0px 6px;
83
- box-shadow: none;
84
- }
85
-
109
+ nile-menu.rte-align-menu::part(menu__items-wrapper) {
110
+ display: flex;
111
+ }
112
+ nile-menu.rte-align-menu,
113
+ nile-menu.rte-default-menu {
114
+ margin-top: 0px;
115
+ }
116
+ nile-button.rte-align-trigger::part(base),
117
+ nile-button.rte-default-trigger::part(base) {
118
+ min-width: 32px;
119
+ height: 32px;
120
+ padding: 0px 6px;
121
+ box-shadow: none;
122
+ }
86
123
  `;
87
-
88
- this.insertBefore(style, this.firstChild);
124
+ this.insertBefore(style, this.firstChild);
89
125
  }
90
126
 
91
-
92
127
  render() {
93
128
  const opts = this.parsedOptions;
94
129
  this.ensureDefault();
95
130
  const current = opts.find(o => o.value === this.selectedValue);
96
131
 
132
+ // ► Align: icon-only items + icon trigger
97
133
  if (this.type === 'align') {
98
- // ► Align: separate dropdown instance + icon-only items + icon trigger
99
134
  const trigger = current?.icon
100
135
  ? html`<nile-icon name="${current.icon}"></nile-icon>`
101
136
  : (this.label || 'Align');
137
+
102
138
  return html`
103
139
  <nile-dropdown class="rte-align-dd">
104
140
  <nile-button slot="trigger" variant="tertiary" class="rte-align-trigger">
@@ -115,33 +151,36 @@ export class NileRteSelect extends LitElement {
115
151
  `)}
116
152
  </nile-menu>
117
153
  </nile-dropdown>
154
+ `;
155
+ }
118
156
 
119
-
157
+ // ► Font: show labels, preview fonts in items and trigger
158
+ if (this.type === 'font') {
159
+ const triggerText = current?.label || this.label || 'Font';
160
+ return html`
161
+ <nile-dropdown class="rte-default-dd">
162
+ <nile-button
163
+ slot="trigger"
164
+ variant="tertiary"
165
+ class="rte-default-trigger"
166
+ style="font-family: ${current?.value || 'inherit'}">
167
+ ${triggerText} <nile-icon name="arrowdown"></nile-icon>
168
+ </nile-button>
169
+ <nile-menu class="rte-default-menu">
170
+ ${opts.map(o => html`
171
+ <nile-menu-item
172
+ style="font-family: ${o.value}"
173
+ ?active=${o.value === this.selectedValue}
174
+ @click=${() => this.onSelect(o.value)}>
175
+ ${o.label}
176
+ </nile-menu-item>
177
+ `)}
178
+ </nile-menu>
179
+ </nile-dropdown>
120
180
  `;
121
- if (this.type === 'font') {
122
- const triggerText = current?.label || this.label || 'Font';
123
- return html`
124
- <nile-dropdown class="rte-default-dd">
125
- <nile-button slot="trigger" variant="tertiary" class="rte-default-trigger"
126
- style="font-family: ${current?.value || 'inherit'}">
127
- ${triggerText} <nile-icon name="arrowdown"></nile-icon>
128
- </nile-button>
129
- <nile-menu class="rte-default-menu">
130
- ${opts.map(o => html`
131
- <nile-menu-item
132
- style="font-family: ${o.value}"
133
- ?active=${o.value === this.selectedValue}
134
- @click=${() => this.onSelect(o.value)}>
135
- ${o.label}
136
- </nile-menu-item>
137
- `)}
138
- </nile-menu>
139
- </nile-dropdown>
140
- `;
141
- }
142
181
  }
143
182
 
144
- // ► Everything else: default (text) dropdown
183
+ // ► Default (e.g., heading): text items; heading values are validated already
145
184
  const triggerText = current?.label || this.label || 'Select';
146
185
  return html`
147
186
  <nile-dropdown class="rte-default-dd">
@@ -102,6 +102,24 @@ export function closestBlock(node: Node | null, root: HTMLElement): HTMLElement
102
102
  }
103
103
  surroundInline(range, 'span', { style: `font-family:${family}` });
104
104
  }
105
+
106
+ export function setBackColor(rootEl: HTMLElement, color: string) {
107
+ const sel = window.getSelection();
108
+ if (!sel || sel.rangeCount === 0) return;
109
+ const range = sel.getRangeAt(0);
110
+ if (!rootEl.contains(range.commonAncestorContainer) || range.collapsed) return;
111
+
112
+ const span = document.createElement('span');
113
+ span.style.backgroundColor = color;
114
+ span.appendChild(range.extractContents());
115
+ range.insertNode(span);
116
+ const after = document.createRange();
117
+ after.setStartAfter(span);
118
+ after.collapse(true);
119
+ sel.removeAllRanges();
120
+ sel.addRange(after);
121
+ }
122
+
105
123
 
106
124
  export function setForeColor(root: HTMLElement, color: string) {
107
125
  const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
@@ -2820,7 +2820,7 @@
2820
2820
  },
2821
2821
  {
2822
2822
  "name": "nile-rich-text-editor",
2823
- "description": "Events:\n\n * `content-changed` {`CustomEvent<{ content: string; }>`} - \n\nAttributes:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\nProperties:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\n * `content` {`string`} - \n\n * `editorEl` {`HTMLElement`} - \n\n * `previewEl` {`HTMLElement | null`} - \n\n * `toolbarEl` {`HTMLElement | null`} - \n\n * `lastRange` {`Range | null`} - \n\n * `buttonMap` {`Map<string, HTMLElement[]>`} - \n\n * `headingSelect` {`HTMLSelectElement | null`} - \n\n * `fontSelect` {`HTMLSelectElement | null`} - \n\n * `colorInput` {`HTMLInputElement | null`} - \n\n * `mentionsEl` {`HTMLElement | null`} - \n\n * `onSelectionChange` - ",
2823
+ "description": "Events:\n\n * `content-changed` {`CustomEvent<{ content: string; }>`} - \n\nAttributes:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\nProperties:\n\n * `value` {`string`} - Initial HTML content\n\n * `mentions` - Optional mentions config (can also be on <nile-rte-mentions mentions=\"...\">)\n\n * `content` {`string`} - \n\n * `editorEl` {`HTMLElement`} - \n\n * `previewEl` {`HTMLElement | null`} - \n\n * `toolbarEl` {`HTMLElement | null`} - \n\n * `lastRange` {`Range | null`} - \n\n * `buttonMap` {`Map<string, HTMLElement[]>`} - \n\n * `headingSelect` {`HTMLSelectElement | null`} - \n\n * `fontSelect` {`HTMLSelectElement | null`} - \n\n * `colorInput` {`HTMLInputElement | null`} - \n\n * `bgColorInput` {`HTMLInputElement | null`} - \n\n * `colorSwatchEl` {`HTMLElement | null`} - \n\n * `bgSwatchEl` {`HTMLElement | null`} - \n\n * `mentionsEl` {`HTMLElement | null`} - \n\n * `onSelectionChange` - ",
2824
2824
  "attributes": [
2825
2825
  {
2826
2826
  "name": "value",
@@ -2900,7 +2900,7 @@
2900
2900
  },
2901
2901
  {
2902
2902
  "name": "nile-rte-select",
2903
- "description": "Events:\n\n * `change` {`CustomEvent<string>`} - \n\nAttributes:\n\n * `type` {`string`} - 'heading' | 'font' | 'align'\n\n * `options` {`string`} - JSON: [{ value, label?, icon? }, ...]\n\n * `label` {`string`} - Fallback label for trigger (e.g., \"Align\")\n\nProperties:\n\n * `type` {`string`} - 'heading' | 'font' | 'align'\n\n * `options` {`string`} - JSON: [{ value, label?, icon? }, ...]\n\n * `label` {`string`} - Fallback label for trigger (e.g., \"Align\")\n\n * `selectedValue` {`string`} - \n\n * `parsedOptions` {`{ value: string; label: string; icon?: string | undefined; }[]`} - ",
2903
+ "description": "Events:\n\n * `change` {`CustomEvent<string>`} - \n\nAttributes:\n\n * `type` {`string`} - 'heading' | 'font' | 'align'\n\n * `options` {`string`} - JSON: [{ value, label?, icon? }, ...] (attribute-based; runtime-validated)\n\n * `label` {`string`} - Fallback label for trigger (e.g., \"Align\")\n\nProperties:\n\n * `type` {`string`} - 'heading' | 'font' | 'align'\n\n * `options` {`string`} - JSON: [{ value, label?, icon? }, ...] (attribute-based; runtime-validated)\n\n * `optionsObj` {`(GenericOption | HeadingOption)[] | undefined`} - Programmatic options (preferred for TS safety).\n\n * `label` {`string`} - Fallback label for trigger (e.g., \"Align\")\n\n * `selectedValue` {`string`} - \n\n * `parsedOptions` {`NormalizedOption[]`} - ",
2904
2904
  "attributes": [
2905
2905
  {
2906
2906
  "name": "type",
@@ -2908,7 +2908,7 @@
2908
2908
  },
2909
2909
  {
2910
2910
  "name": "options",
2911
- "description": "`options` {`string`} - JSON: [{ value, label?, icon? }, ...]\n\nProperty: options\n\nDefault: []"
2911
+ "description": "`options` {`string`} - JSON: [{ value, label?, icon? }, ...] (attribute-based; runtime-validated)\n\nProperty: options\n\nDefault: []"
2912
2912
  },
2913
2913
  {
2914
2914
  "name": "label",