smeditor 0.2.0 → 0.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd12092236abeb99040e428490e6ef352721d4bb266b0a2933ca22a413350941
4
- data.tar.gz: 8f3e32940dc8fdc7e37a26765d9925b8a0755319714a2d623ea1b8f8fd046091
3
+ metadata.gz: deea7d855b9ea5c7d61a51d82adeb656e3583db0dc9ed8d677345fe2d1bac550
4
+ data.tar.gz: 99a68a8823548919c2acb1aaea114822b677e33d464cd37a66f42fe4cc802e73
5
5
  SHA512:
6
- metadata.gz: 4fc1cb9a65be6f82544f1be230cb860fd0525df94a401b76921c2664e40a5e4587978cf9d3a2c0d011b230d0a9e7f1b89547cf38063d52ffb8614ef5ad7961b6
7
- data.tar.gz: 4d72b4392ec27c3a13c46510da58a7266dbeeebaef74ef422c9ff7c2b15e7fff4ef499ad3d6cb839f9b31631b334d166af18c2e84f43ec5e22efc8609f4688a1
6
+ metadata.gz: 6cf34629807ab5b326dff8ddaa85e80bc2f4b5c7de86a233e4d2a9c1196a3151df18af818f658f6826e0f21c7d90317c8f190df1697f5ecc8501c4f24511c9b7
7
+ data.tar.gz: 3f72f9d9fc819294563dd2519eb07e84dd159432fd2bc8ad6111b0ed125c46790163ef7caae49ae1b76ea7ced5406baf65b1dc9a2350e6115edc15b34c3d57bc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog — smeditor
2
2
 
3
+ ## 0.2.2 — 2026-09-09
4
+
5
+ - Fixed caret jumps and reordered typing inside the Rails Shadow DOM editor. The core now resolves the live DOM Selection from the editor's own root instead of always using `document.getSelection()`.
6
+ - Selection restore and caret coordinate ranges now use the editor element's owner document, keeping the DOM integration correct in isolated roots and embedded documents.
7
+
8
+ ## 0.2.1 — 2026-09-09
9
+
10
+ - Rails editor now renders inside Shadow DOM so host application CSS cannot alter the SMEditor toolbar or editing surface.
11
+ - Rails toolbar now uses the same SMEditor visual language and control layout as the playground instead of native selects/text buttons.
12
+ - Theme tokens work inside the isolated editor and continue to follow light/dark preference.
13
+ - The self-contained gem still requires no npm, React, esbuild, or jsbundling-rails in the host Rails application.
14
+
15
+
3
16
  All notable changes to the Rails adapter are recorded here. The format is
4
17
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the gem
5
18
  follows [RubyGems versioning](https://guides.rubygems.org/patterns/).
@@ -13,6 +13,53 @@ const starter_kit_1 = require("@smeditor/starter-kit");
13
13
  const full_kit_1 = require("@smeditor/full-kit");
14
14
  const instances = new Map();
15
15
  const disposers = new Map();
16
+ const ICONS = {
17
+ undo: '<path d="M3.5 8h7a3 3 0 0 1 0 6H6"/><path d="m6 4-3 4 3 4"/>',
18
+ redo: '<path d="M12.5 8h-7a3 3 0 0 0 0 6H10"/><path d="m10 4 3 4-3 4"/>',
19
+ bold: '<path d="M4 3h4.5a2.5 2.5 0 0 1 0 5H4z"/><path d="M4 8h5a2.5 2.5 0 0 1 0 5H4z"/>',
20
+ italic: '<path d="M10 3 6 13"/><path d="M6 3h5"/><path d="M5 13h5"/>',
21
+ strike: '<path d="M3 8h10"/><path d="M5.5 4.5C6 4 6.7 3.5 8 3.5c2 0 3 1 3 2.5"/><path d="M5 11c0 1.4 1.3 2.5 3 2.5 1.8 0 3-1 3-2.2"/>',
22
+ underline: '<path d="M4 3v5a4 4 0 0 0 8 0V3"/><path d="M3.5 13.5h9"/>',
23
+ code: '<path d="m6 5-3 3 3 3"/><path d="m10 5 3 3-3 3"/>',
24
+ eraser: '<path d="m9 3 5 5-6 6H4l-1-1z"/><path d="m6 6 5 5"/><path d="M9 14h5"/>',
25
+ link: '<path d="M7 9a3 3 0 0 0 4 0l2-2a3 3 0 1 0-4-4L8 4"/><path d="M9 7a3 3 0 0 0-4 0l-2 2a3 3 0 1 0 4 4l1-1"/>',
26
+ highlight: '<path d="M3 13l3-1 6-6-2-2-6 6z"/><path d="M2 14h12"/>',
27
+ textColor: '<text x="3" y="11" font-size="9" font-weight="700" fill="currentColor" stroke="none">A</text><rect x="3" y="13" width="10" height="2" fill="currentColor" stroke="none"/>',
28
+ background: '<rect x="2" y="2" width="12" height="12" rx="1.5"/><path d="M2 2l12 12"/>',
29
+ blockquote: '<path d="M3 5v3c0 1.5-.7 2.5-2 3"/><path d="M9 5v3c0 1.5-.7 2.5-2 3"/>',
30
+ codeBlock: '<rect x="1.5" y="3" width="13" height="10" rx="1.5"/><path d="m5 6.5-2 1.5 2 1.5"/><path d="m11 6.5 2 1.5-2 1.5"/>',
31
+ hr: '<path d="M2 8h12"/><path d="M2 4h8"/><path d="M2 12h6"/>',
32
+ bullet: '<circle cx="3.5" cy="4.5" r=".7" fill="currentColor"/><circle cx="3.5" cy="8" r=".7" fill="currentColor"/><circle cx="3.5" cy="11.5" r=".7" fill="currentColor"/><path d="M6 4.5h7M6 8h7M6 11.5h7"/>',
33
+ ordered: '<text x="2" y="6" font-size="4" font-family="monospace" fill="currentColor" stroke="none">1.</text><text x="2" y="13" font-size="4" font-family="monospace" fill="currentColor" stroke="none">2.</text><path d="M6 4.5h7M6 11.5h7"/>',
34
+ task: '<rect x="2" y="3" width="3" height="3" rx=".5"/><path d="m2.5 4.5 1 1 1.5-1.5"/><rect x="2" y="10" width="3" height="3" rx=".5"/><path d="M7 4.5h6M7 11.5h6"/>',
35
+ alignLeft: '<path d="M2.5 4h11M2.5 7h7M2.5 10h11M2.5 13h7"/>',
36
+ alignCenter: '<path d="M2.5 4h11M4.5 7h7M2.5 10h11M4.5 13h7"/>',
37
+ alignRight: '<path d="M2.5 4h11M6.5 7h7M2.5 10h11M6.5 13h7"/>',
38
+ alignJustify: '<path d="M2.5 4h11M2.5 7h11M2.5 10h11M2.5 13h11"/>',
39
+ indent: '<path d="M2 4h12M7 8h7M2 12h12"/><path d="m2 7 2 1.5L2 10" fill="currentColor"/>',
40
+ outdent: '<path d="M2 4h12M7 8h7M2 12h12"/><path d="m6 7-2 1.5L6 10" fill="currentColor"/>',
41
+ image: '<rect x="2" y="3" width="12" height="10" rx="1.5"/><circle cx="5.5" cy="6.5" r="1.2"/><path d="m2.5 11 3.5-3.5 2.5 2.5L11 7.5l2.5 2.5"/>',
42
+ table: '<rect x="2" y="3" width="12" height="10" rx="1"/><path d="M2 6.5h12M2 10h12M6 3v10M10 3v10"/>',
43
+ more: '<circle cx="3.5" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="12.5" cy="8" r="1.2" fill="currentColor"/>',
44
+ heading: '<text x="2" y="12" font-family="sans-serif" font-size="9" font-weight="700" fill="currentColor" stroke="none">H</text><text x="9.5" y="13" font-family="sans-serif" font-size="6" font-weight="700" fill="currentColor" stroke="none">2</text>',
45
+ paragraph: '<text x="3" y="12" font-family="serif" font-size="11" font-weight="700" fill="currentColor" stroke="none">¶</text>',
46
+ subscript: '<path d="M3 4l4 5"/><path d="M7 4l-4 5"/><text x="9" y="14" font-size="6" font-weight="700" fill="currentColor" stroke="none">x</text>',
47
+ superscript: '<path d="M3 6l4 5"/><path d="M7 6l-4 5"/><text x="9" y="7" font-size="6" font-weight="700" fill="currentColor" stroke="none">x</text>',
48
+ };
49
+ function svgIcon(name, strokeWidth = 1.5) {
50
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
51
+ svg.setAttribute("width", "16");
52
+ svg.setAttribute("height", "16");
53
+ svg.setAttribute("viewBox", "0 0 16 16");
54
+ svg.setAttribute("fill", "none");
55
+ svg.setAttribute("stroke", "currentColor");
56
+ svg.setAttribute("stroke-width", String(strokeWidth));
57
+ svg.setAttribute("stroke-linecap", "round");
58
+ svg.setAttribute("stroke-linejoin", "round");
59
+ svg.setAttribute("aria-hidden", "true");
60
+ svg.innerHTML = ICONS[name] || "";
61
+ return svg;
62
+ }
16
63
  function csrfToken() {
17
64
  var _a, _b;
18
65
  return (_b = (_a = document.querySelector('meta[name="csrf-token"]')) === null || _a === void 0 ? void 0 : _a.content) !== null && _b !== void 0 ? _b : null;
@@ -45,16 +92,16 @@ function kitExtensions(kit, uploadUrl) {
45
92
  return [...base];
46
93
  return base.map((extension) => extension.name === "image" ? starter_kit_1.ImageExtension.configure({ upload }) : extension);
47
94
  }
48
- function button(label, title, action) {
49
- const el = document.createElement("button");
50
- el.type = "button";
51
- el.className = "smeditor-button";
52
- el.textContent = label;
53
- el.title = title;
54
- el.setAttribute("aria-label", title);
55
- el.addEventListener("mousedown", (event) => event.preventDefault());
56
- el.addEventListener("click", () => action());
57
- return el;
95
+ function run(editor, command, ...args) {
96
+ const fn = editor.commands[command];
97
+ if (typeof fn !== "function")
98
+ return false;
99
+ const result = fn(...args);
100
+ editor.focus();
101
+ return Boolean(result);
102
+ }
103
+ function hasCommand(editor, command) {
104
+ return typeof editor.commands[command] === "function";
58
105
  }
59
106
  function divider() {
60
107
  const el = document.createElement("span");
@@ -63,170 +110,358 @@ function divider() {
63
110
  el.setAttribute("aria-hidden", "true");
64
111
  return el;
65
112
  }
66
- function selectControl(label, options, onChange) {
67
- const select = document.createElement("select");
68
- select.className = "smeditor-select";
69
- select.title = label;
70
- select.setAttribute("aria-label", label);
71
- for (const [value, text] of options) {
72
- const option = document.createElement("option");
73
- option.value = value;
74
- option.textContent = text;
75
- select.appendChild(option);
76
- }
77
- select.addEventListener("change", () => {
78
- onChange(select.value);
79
- select.value = "";
80
- });
81
- return select;
113
+ function commandButton(editor, iconName, title, command, active, label) {
114
+ const el = document.createElement("button");
115
+ el.type = "button";
116
+ el.className = "smeditor-button";
117
+ el.title = title;
118
+ el.setAttribute("aria-label", title);
119
+ if (label)
120
+ el.textContent = label;
121
+ else
122
+ el.appendChild(svgIcon(iconName, iconName === "bold" ? 2 : 1.5));
123
+ el.addEventListener("mousedown", (event) => event.preventDefault());
124
+ el.addEventListener("click", () => command());
125
+ const refresh = () => {
126
+ if (active) {
127
+ const value = active();
128
+ el.dataset.active = value ? "true" : "false";
129
+ el.classList.toggle("is-active", value);
130
+ }
131
+ };
132
+ el.__smeditorRefresh = refresh;
133
+ refresh();
134
+ return el;
82
135
  }
83
- function run(editor, command, ...args) {
84
- const fn = editor.commands[command];
85
- if (typeof fn !== "function")
86
- return false;
87
- const result = fn(...args);
88
- editor.focus();
89
- return Boolean(result);
136
+ function dropdown(ariaLabel, triggerContent, items, options = {}) {
137
+ const wrapper = document.createElement("div");
138
+ wrapper.className = "smeditor-dropdown";
139
+ const trigger = document.createElement("button");
140
+ trigger.type = "button";
141
+ trigger.className = "smeditor-button smeditor-dropdown__trigger";
142
+ trigger.setAttribute("aria-haspopup", "menu");
143
+ trigger.setAttribute("aria-expanded", "false");
144
+ trigger.setAttribute("aria-label", ariaLabel);
145
+ trigger.title = ariaLabel;
146
+ const label = document.createElement("span");
147
+ label.className = "smeditor-dropdown__label";
148
+ if (typeof triggerContent === "string")
149
+ label.textContent = triggerContent;
150
+ else
151
+ label.appendChild(triggerContent);
152
+ trigger.appendChild(label);
153
+ let summary = null;
154
+ if (options.summary) {
155
+ summary = document.createElement("span");
156
+ summary.className = "smeditor-dropdown__summary";
157
+ trigger.appendChild(summary);
158
+ }
159
+ const chevron = document.createElement("span");
160
+ chevron.className = "smeditor-dropdown__chevron";
161
+ chevron.setAttribute("aria-hidden", "true");
162
+ chevron.textContent = "▾";
163
+ trigger.appendChild(chevron);
164
+ wrapper.appendChild(trigger);
165
+ let menu = null;
166
+ const close = () => {
167
+ wrapper.classList.remove("is-open");
168
+ trigger.setAttribute("aria-expanded", "false");
169
+ menu === null || menu === void 0 ? void 0 : menu.remove();
170
+ menu = null;
171
+ };
172
+ const open = () => {
173
+ var _a, _b, _c, _d;
174
+ if (menu)
175
+ return close();
176
+ wrapper.classList.add("is-open");
177
+ trigger.setAttribute("aria-expanded", "true");
178
+ menu = document.createElement("div");
179
+ menu.className = "smeditor-dropdown__menu" + (options.align === "right" ? " is-right" : "");
180
+ menu.setAttribute("role", "menu");
181
+ menu.setAttribute("aria-label", ariaLabel);
182
+ for (const spec of items()) {
183
+ if (spec.separatorBefore) {
184
+ const sep = document.createElement("div");
185
+ sep.className = "smeditor-dropdown__separator";
186
+ sep.setAttribute("role", "separator");
187
+ menu.appendChild(sep);
188
+ }
189
+ if (spec.content) {
190
+ menu.appendChild(spec.content);
191
+ continue;
192
+ }
193
+ const item = document.createElement("button");
194
+ item.type = "button";
195
+ item.className = "smeditor-dropdown__item";
196
+ item.setAttribute("role", spec.active ? "menuitemcheckbox" : "menuitem");
197
+ const isActive = (_b = (_a = spec.active) === null || _a === void 0 ? void 0 : _a.call(spec)) !== null && _b !== void 0 ? _b : false;
198
+ if (spec.active)
199
+ item.setAttribute("aria-checked", String(isActive));
200
+ item.classList.toggle("is-active", isActive);
201
+ item.dataset.active = isActive ? "true" : "false";
202
+ const isDisabled = (_d = (_c = spec.disabled) === null || _c === void 0 ? void 0 : _c.call(spec)) !== null && _d !== void 0 ? _d : false;
203
+ item.disabled = isDisabled;
204
+ if (isDisabled)
205
+ item.setAttribute("aria-disabled", "true");
206
+ if (spec.icon) {
207
+ const icon = document.createElement("span");
208
+ icon.className = "smeditor-dropdown__item-icon";
209
+ icon.appendChild(svgIcon(spec.icon));
210
+ item.appendChild(icon);
211
+ }
212
+ const text = document.createElement("span");
213
+ text.className = "smeditor-dropdown__item-label";
214
+ text.textContent = spec.label;
215
+ item.appendChild(text);
216
+ if (spec.shortcut) {
217
+ const shortcut = document.createElement("span");
218
+ shortcut.className = "smeditor-dropdown__item-shortcut";
219
+ shortcut.textContent = spec.shortcut;
220
+ item.appendChild(shortcut);
221
+ }
222
+ item.addEventListener("mousedown", (event) => event.preventDefault());
223
+ item.addEventListener("click", () => {
224
+ var _a;
225
+ if (item.disabled)
226
+ return;
227
+ (_a = spec.action) === null || _a === void 0 ? void 0 : _a.call(spec);
228
+ close();
229
+ });
230
+ menu.appendChild(item);
231
+ }
232
+ wrapper.appendChild(menu);
233
+ };
234
+ trigger.addEventListener("mousedown", (event) => event.preventDefault());
235
+ trigger.addEventListener("click", (event) => {
236
+ event.stopPropagation();
237
+ open();
238
+ });
239
+ const outside = (event) => {
240
+ if (!menu)
241
+ return;
242
+ const path = event.composedPath();
243
+ if (!path.includes(wrapper))
244
+ close();
245
+ };
246
+ document.addEventListener("pointerdown", outside, true);
247
+ const refresh = () => {
248
+ if (summary && options.summary)
249
+ summary.textContent = options.summary();
250
+ if (options.active) {
251
+ const value = options.active();
252
+ trigger.dataset.active = value ? "true" : "false";
253
+ trigger.classList.toggle("is-active", value);
254
+ }
255
+ };
256
+ wrapper.__smeditorRefresh = refresh;
257
+ wrapper.__smeditorDispose = () => document.removeEventListener("pointerdown", outside, true);
258
+ refresh();
259
+ return wrapper;
260
+ }
261
+ function colorDropdown(editor, ariaLabel, iconName, markName, setCommand, unsetCommand, extraAttrs = {}) {
262
+ const colors = [
263
+ "#1a1a1f", "#6b7280", "#ef4444", "#f97316", "#eab308",
264
+ "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899",
265
+ "#ffffff", "#d1d5db", "#fecaca", "#fed7aa", "#fef08a",
266
+ "#bbf7d0", "#a5f3fc", "#bfdbfe", "#ddd6fe", "#fbcfe8",
267
+ ];
268
+ return dropdown(ariaLabel, svgIcon(iconName), () => {
269
+ const grid = document.createElement("div");
270
+ grid.className = "smeditor-color-grid";
271
+ grid.setAttribute("role", "group");
272
+ const clear = document.createElement("button");
273
+ clear.type = "button";
274
+ clear.className = "smeditor-color-grid__swatch smeditor-color-grid__swatch--clear";
275
+ clear.title = `Clear ${ariaLabel.toLowerCase()}`;
276
+ clear.setAttribute("aria-label", clear.title);
277
+ clear.addEventListener("mousedown", (e) => e.preventDefault());
278
+ clear.addEventListener("click", () => run(editor, unsetCommand));
279
+ grid.appendChild(clear);
280
+ for (const color of colors) {
281
+ const swatch = document.createElement("button");
282
+ swatch.type = "button";
283
+ swatch.className = "smeditor-color-grid__swatch";
284
+ swatch.style.backgroundColor = color;
285
+ swatch.title = color;
286
+ swatch.setAttribute("aria-label", `${ariaLabel}: ${color}`);
287
+ swatch.addEventListener("mousedown", (e) => e.preventDefault());
288
+ swatch.addEventListener("click", () => run(editor, setCommand, { color, ...extraAttrs }));
289
+ grid.appendChild(swatch);
290
+ }
291
+ return [{ label: "", content: grid }];
292
+ }, { active: () => editor.isActive(markName) });
293
+ }
294
+ function blockTypeSummary(editor) {
295
+ if ((0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "blockquote"))
296
+ return "❝";
297
+ if (editor.isActive("code_block"))
298
+ return "</>";
299
+ for (const level of [1, 2, 3, 4, 5, 6]) {
300
+ if (editor.isActive("heading", { level }))
301
+ return `H${level}`;
302
+ }
303
+ return "P";
304
+ }
305
+ function tablePicker(editor) {
306
+ return dropdown("Insert table", svgIcon("table"), () => {
307
+ const grid = document.createElement("div");
308
+ grid.className = "smeditor-table-picker";
309
+ grid.setAttribute("role", "group");
310
+ grid.setAttribute("aria-label", "Insert table size");
311
+ const cells = [];
312
+ const refreshHover = (rows, cols) => {
313
+ cells.forEach((cell, idx) => {
314
+ const r = Math.floor(idx / 6) + 1;
315
+ const c = (idx % 6) + 1;
316
+ cell.classList.toggle("is-on", r <= rows && c <= cols);
317
+ });
318
+ label.textContent = rows && cols ? `${rows} × ${cols}` : "Select size";
319
+ };
320
+ for (let r = 1; r <= 6; r++) {
321
+ for (let c = 1; c <= 6; c++) {
322
+ const cell = document.createElement("button");
323
+ cell.type = "button";
324
+ cell.className = "smeditor-table-picker__cell";
325
+ cell.setAttribute("aria-label", `Insert ${r} by ${c} table`);
326
+ cell.addEventListener("mouseenter", () => refreshHover(r, c));
327
+ cell.addEventListener("mousedown", (e) => e.preventDefault());
328
+ cell.addEventListener("click", () => run(editor, "insertTable", { rows: r, cols: c }));
329
+ cells.push(cell);
330
+ grid.appendChild(cell);
331
+ }
332
+ }
333
+ const label = document.createElement("div");
334
+ label.className = "smeditor-table-picker__label";
335
+ label.textContent = "Select size";
336
+ const box = document.createElement("div");
337
+ box.append(grid, label);
338
+ box.addEventListener("mouseleave", () => refreshHover(0, 0));
339
+ return [{ label: "", content: box }];
340
+ });
90
341
  }
91
- function activeButton(editor, el, name, attrs) {
92
- el.dataset.active = editor.isActive(name, attrs) ? "true" : "false";
342
+ function textDropdown(editor, ariaLabel, label, values, command, unset) {
343
+ return dropdown(ariaLabel, label, () => values.map(([text, value]) => ({
344
+ label: text,
345
+ action: () => value === null && unset ? run(editor, unset) : run(editor, command, { [command === "setFontFamily" ? "family" : command === "setFontSize" ? "size" : "value"]: value }),
346
+ })));
93
347
  }
94
348
  function buildToolbar(editor, kit, uploadUrl) {
95
- var _a;
96
349
  const toolbar = document.createElement("div");
97
350
  toolbar.className = "smeditor-toolbar";
98
351
  toolbar.setAttribute("role", "toolbar");
99
352
  toolbar.setAttribute("aria-label", "SMEditor toolbar");
100
- const undo = button("", "Undo", () => run(editor, "undo"));
101
- const redo = button("↷", "Redo", () => run(editor, "redo"));
102
- toolbar.append(undo, redo, divider());
103
- const block = selectControl("Block type", [
104
- ["", "Block"], ["p", "Paragraph"], ["h1", "Heading 1"], ["h2", "Heading 2"],
105
- ["h3", "Heading 3"], ["h4", "Heading 4"], ["quote", "Blockquote"], ["code", "Code block"],
106
- ], (value) => {
107
- if (value === "p")
108
- run(editor, "setParagraph");
109
- else if (/^h[1-6]$/.test(value))
110
- run(editor, "setHeading", { level: Number(value.slice(1)) });
111
- else if (value === "quote")
112
- run(editor, "toggleBlockquote");
113
- else if (value === "code")
114
- run(editor, "setCodeBlock");
115
- });
116
- toolbar.append(block, divider());
117
- const bold = button("B", "Bold", () => run(editor, "toggleBold"));
118
- const italic = button("I", "Italic", () => run(editor, "toggleItalic"));
119
- const underline = button("U", "Underline", () => run(editor, "toggleUnderline"));
120
- const strike = button("S", "Strike", () => run(editor, "toggleStrike"));
121
- bold.style.fontWeight = "700";
122
- italic.style.fontStyle = "italic";
123
- underline.style.textDecoration = "underline";
124
- strike.style.textDecoration = "line-through";
125
- toolbar.append(bold, italic, underline, strike);
126
- if (kit === "full") {
127
- const inlineCode = button("</>", "Inline code", () => run(editor, "toggleCode"));
128
- const sub = button("x₂", "Subscript", () => run(editor, "toggleSubscript"));
129
- const sup = button("x²", "Superscript", () => run(editor, "toggleSuperscript"));
130
- toolbar.append(inlineCode, sub, sup);
131
- }
132
- const clear = button("Tx", "Clear formatting", () => run(editor, "clearFormatting"));
133
- const link = button("🔗", "Link", () => {
353
+ toolbar.setAttribute("aria-orientation", "horizontal");
354
+ const controls = [];
355
+ const add = (...els) => { toolbar.append(...els); controls.push(...els); };
356
+ const undo = commandButton(editor, "undo", "Undo", () => run(editor, "undo"));
357
+ const redo = commandButton(editor, "redo", "Redo", () => run(editor, "redo"));
358
+ add(undo, redo, divider());
359
+ const block = dropdown("Block type", svgIcon("heading"), () => {
360
+ const items = [{ label: "Paragraph", icon: "paragraph", active: () => editor.isActive("paragraph"), action: () => run(editor, "setParagraph") }];
361
+ for (let level = 1; level <= 6; level++)
362
+ items.push({ label: `Heading ${level}`, icon: "heading", shortcut: `⌘⌥${level}`, active: () => editor.isActive("heading", { level }), action: () => run(editor, "setHeading", { level }), separatorBefore: level === 1 });
363
+ items.push({ label: "Quote", icon: "blockquote", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "blockquote"), action: () => run(editor, "toggleBlockquote"), separatorBefore: true });
364
+ items.push({ label: "Code block", icon: "codeBlock", active: () => editor.isActive("code_block"), action: () => run(editor, "setCodeBlock") });
365
+ return items;
366
+ }, { summary: () => blockTypeSummary(editor) });
367
+ const lists = dropdown("Lists", svgIcon("bullet"), () => [
368
+ { label: "Bullet List", icon: "bullet", shortcut: "⌘⇧8", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "bullet_list"), action: () => run(editor, "toggleBulletList") },
369
+ { label: "Ordered List", icon: "ordered", shortcut: "⌘⇧7", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "ordered_list"), action: () => run(editor, "toggleOrderedList") },
370
+ { label: "Task List", icon: "task", shortcut: "⌘⇧9", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "task_list"), action: () => run(editor, "toggleTaskList") },
371
+ ], { active: () => ["bullet_list", "ordered_list", "task_list"].some((name) => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), name)) });
372
+ add(block, lists, divider());
373
+ add(commandButton(editor, "bold", "Bold", () => run(editor, "toggleBold"), () => editor.isActive("bold")), commandButton(editor, "italic", "Italic", () => run(editor, "toggleItalic"), () => editor.isActive("italic")), commandButton(editor, "strike", "Strike-through", () => run(editor, "toggleStrike"), () => editor.isActive("strike")), commandButton(editor, "underline", "Underline", () => run(editor, "toggleUnderline"), () => editor.isActive("underline")), commandButton(editor, "eraser", "Clear formatting", () => run(editor, "clearFormatting")), commandButton(editor, "link", "Link", () => {
134
374
  if (editor.isActive("link"))
135
375
  return run(editor, "unsetLink");
136
376
  const href = window.prompt("Link URL", "https://");
137
- if (href)
138
- return run(editor, "setLink", { href });
139
- });
140
- toolbar.append(clear, link, divider());
141
- const list = selectControl("List", [["", "List"], ["bullet", "Bulleted"], ["ordered", "Numbered"], ["task", "Task list"]], (value) => {
142
- if (value === "bullet")
143
- run(editor, "toggleBulletList");
144
- if (value === "ordered")
145
- run(editor, "toggleOrderedList");
146
- if (value === "task")
147
- run(editor, "toggleTaskList");
148
- });
149
- const align = selectControl("Alignment", [["", "Align"], ["left", "Left"], ["center", "Center"], ["right", "Right"], ["justify", "Justify"]], (value) => {
150
- if (value)
151
- run(editor, "setTextAlign", { align: value });
152
- });
153
- toolbar.append(list, align, divider());
154
- toolbar.append(button("❝", "Blockquote", () => run(editor, "toggleBlockquote")), button("{ }", "Code block", () => run(editor, "setCodeBlock")), button("―", "Horizontal rule", () => run(editor, "insertHorizontalRule")));
155
- const image = button("Image", "Insert image by URL", () => {
156
- const src = window.prompt("Image URL", "https://");
157
- if (src)
158
- run(editor, "insertImage", { src });
377
+ return href ? run(editor, "setLink", { href }) : false;
378
+ }, () => editor.isActive("link")), divider());
379
+ const align = dropdown("Alignment", svgIcon("alignLeft"), () => [
380
+ { label: "Left", icon: "alignLeft", active: () => editor.isActive("text_align", { align: "left" }), action: () => run(editor, "setTextAlign", { align: "left" }) },
381
+ { label: "Center", icon: "alignCenter", active: () => editor.isActive("text_align", { align: "center" }), action: () => run(editor, "setTextAlign", { align: "center" }) },
382
+ { label: "Right", icon: "alignRight", active: () => editor.isActive("text_align", { align: "right" }), action: () => run(editor, "setTextAlign", { align: "right" }) },
383
+ { label: "Justify", icon: "alignJustify", active: () => editor.isActive("text_align", { align: "justify" }), action: () => run(editor, "setTextAlign", { align: "justify" }) },
384
+ { label: "Increase indent", icon: "indent", action: () => run(editor, "indent"), separatorBefore: true },
385
+ { label: "Decrease indent", icon: "outdent", action: () => run(editor, "outdent") },
386
+ ]);
387
+ add(align, divider());
388
+ add(commandButton(editor, "blockquote", "Blockquote", () => run(editor, "toggleBlockquote"), () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "blockquote")), commandButton(editor, "codeBlock", "Code block", () => run(editor, "setCodeBlock"), () => editor.isActive("code_block")), commandButton(editor, "hr", "Horizontal rule", () => run(editor, "insertHorizontalRule")));
389
+ const image = dropdown("Insert image", svgIcon("image"), () => {
390
+ const items = [{ label: "From URL…", icon: "link", action: () => {
391
+ var _a;
392
+ const src = window.prompt("Image URL", "https://");
393
+ if (!src)
394
+ return;
395
+ const alt = (_a = window.prompt("Alt text (optional)", "")) !== null && _a !== void 0 ? _a : "";
396
+ run(editor, "insertImage", { src, alt: alt || undefined });
397
+ } }];
398
+ if (uploadUrl && hasCommand(editor, "uploadImage"))
399
+ items.push({ label: "Upload image…", icon: "image", action: () => {
400
+ const input = document.createElement("input");
401
+ input.type = "file";
402
+ input.accept = "image/*";
403
+ input.onchange = () => { var _a; const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0]; if (file)
404
+ run(editor, "uploadImage", file); };
405
+ input.click();
406
+ } });
407
+ return items;
159
408
  });
160
- toolbar.append(image);
161
- if (uploadUrl && typeof editor.commands.uploadImage === "function") {
162
- const file = document.createElement("input");
163
- file.type = "file";
164
- file.accept = "image/*";
165
- file.className = "smeditor-file-input";
166
- file.tabIndex = -1;
167
- file.setAttribute("aria-hidden", "true");
168
- const upload = button("Upload", "Upload image", () => file.click());
169
- file.addEventListener("change", () => {
170
- var _a;
171
- const selected = (_a = file.files) === null || _a === void 0 ? void 0 : _a[0];
172
- if (selected)
173
- run(editor, "uploadImage", selected);
174
- file.value = "";
175
- });
176
- toolbar.append(upload, file);
177
- }
178
- toolbar.append(button("Table", "Insert table", () => {
179
- const rows = Number(window.prompt("Rows", "3") || "3");
180
- const cols = Number(window.prompt("Columns", "3") || "3");
181
- run(editor, "insertTable", { rows, cols, withHeaderRow: true });
182
- }));
409
+ add(image, tablePicker(editor));
183
410
  if (kit === "full") {
184
- toolbar.append(divider());
185
- const font = selectControl("Font family", [
186
- ["", "Font"], ["Arial", "Arial"], ["Georgia", "Georgia"], ["Times New Roman", "Times"],
187
- ["Verdana", "Verdana"], ["Courier New", "Courier"],
188
- ], (value) => value && run(editor, "setFontFamily", { family: value }));
189
- const size = selectControl("Font size", [["", "Size"], ["12px", "12"], ["14px", "14"], ["16px", "16"], ["18px", "18"], ["24px", "24"], ["32px", "32"]], (value) => value && run(editor, "setFontSize", { size: value }));
190
- const line = selectControl("Line height", [["", "Line"], ["1", "1.0"], ["1.25", "1.25"], ["1.5", "1.5"], ["1.75", "1.75"], ["2", "2.0"]], (value) => value && run(editor, "setLineHeight", { value }));
191
- toolbar.append(font, size, line);
192
- const textColor = document.createElement("input");
193
- textColor.type = "color";
194
- textColor.className = "smeditor-color-input";
195
- textColor.title = "Text color";
196
- textColor.setAttribute("aria-label", "Text color");
197
- textColor.addEventListener("input", () => run(editor, "setTextColor", { color: textColor.value }));
198
- const bgColor = document.createElement("input");
199
- bgColor.type = "color";
200
- bgColor.className = "smeditor-color-input";
201
- bgColor.title = "Background color";
202
- bgColor.setAttribute("aria-label", "Background color");
203
- bgColor.addEventListener("input", () => run(editor, "setBackgroundColor", { color: bgColor.value }));
204
- toolbar.append(textColor, bgColor);
205
- }
206
- const updateState = () => {
207
- undo.disabled = !editor.canUndo();
208
- redo.disabled = !editor.canRedo();
209
- activeButton(editor, bold, "bold");
210
- activeButton(editor, italic, "italic");
211
- activeButton(editor, underline, "underline");
212
- activeButton(editor, strike, "strike");
213
- link.dataset.active = editor.isActive("link") ? "true" : "false";
411
+ add(divider());
412
+ add(colorDropdown(editor, "Highlight", "highlight", "highlight", "setHighlight", "unsetHighlight"), colorDropdown(editor, "Text color", "textColor", "text_color", "setTextColor", "unsetTextColor"), colorDropdown(editor, "Text stroke", "textColor", "text_stroke", "setTextStroke", "unsetTextStroke", { width: 1 }), colorDropdown(editor, "Background color", "background", "background_color", "setBackgroundColor", "unsetBackgroundColor"), textDropdown(editor, "Font family", "Font", [["Default", null], ["Sans-serif", "ui-sans-serif, system-ui, sans-serif"], ["Serif", "ui-serif, Georgia, serif"], ["Monospace", "ui-monospace, Menlo, monospace"], ["Georgia", "Georgia, serif"], ["Courier", "'Courier New', monospace"]], "setFontFamily", "unsetFontFamily"), textDropdown(editor, "Font size", "Size", [["Default", null], ["Small · 13px", "13px"], ["Normal · 16px", "16px"], ["Large · 20px", "20px"], ["Huge · 28px", "28px"]], "setFontSize", "unsetFontSize"), textDropdown(editor, "Line height", "Spacing", [["Default", null], ["Tight · 1", "1"], ["Snug · 1.15", "1.15"], ["Normal · 1.5", "1.5"], ["Relaxed · 2", "2"]], "setLineHeight"));
413
+ if (hasCommand(editor, "addComment")) {
414
+ add(commandButton(editor, "", "Comment", () => {
415
+ const body = window.prompt("New comment");
416
+ if (!body)
417
+ return false;
418
+ return run(editor, "addComment", { threadId: `comment-${Date.now()}` });
419
+ }, undefined, "Comment"));
420
+ }
421
+ const more = dropdown("More tools", svgIcon("more"), () => [
422
+ { label: "Inline code", icon: "code", shortcut: "⌘E", active: () => editor.isActive("code"), action: () => run(editor, "toggleCode") },
423
+ { label: "Subscript", icon: "subscript", active: () => editor.isActive("subscript"), action: () => run(editor, "toggleSubscript"), separatorBefore: true },
424
+ { label: "Superscript", icon: "superscript", active: () => editor.isActive("superscript"), action: () => run(editor, "toggleSuperscript") },
425
+ { label: "Increase indent", icon: "indent", action: () => run(editor, "indent"), separatorBefore: true },
426
+ { label: "Decrease indent", icon: "outdent", action: () => run(editor, "outdent") },
427
+ ], { align: "right" });
428
+ add(more);
429
+ }
430
+ const refresh = () => {
431
+ var _a, _b;
432
+ undo.toggleAttribute("disabled", !editor.canUndo());
433
+ redo.toggleAttribute("disabled", !editor.canRedo());
434
+ for (const control of controls)
435
+ (_b = (_a = control).__smeditorRefresh) === null || _b === void 0 ? void 0 : _b.call(_a);
436
+ };
437
+ const disposeUpdate = editor.on("update", refresh);
438
+ const disposeSelection = editor.on("selectionUpdate", refresh);
439
+ queueMicrotask(refresh);
440
+ toolbar.__smeditorDispose = () => {
441
+ var _a, _b;
442
+ disposeUpdate();
443
+ disposeSelection();
444
+ for (const control of controls)
445
+ (_b = (_a = control).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a);
214
446
  };
215
- const disposeUpdate = editor.on("update", updateState);
216
- const disposeSelection = editor.on("selectionUpdate", updateState);
217
- queueMicrotask(updateState);
218
- const listForMount = (_a = disposers.get(editor.element)) !== null && _a !== void 0 ? _a : [];
219
- listForMount.push(disposeUpdate, disposeSelection);
220
- disposers.set(editor.element, listForMount);
221
447
  return toolbar;
222
448
  }
449
+ function shadowRootFor(mount) {
450
+ const root = mount.shadowRoot || mount.attachShadow({ mode: "open" });
451
+ root.innerHTML = "";
452
+ const stylesheet = mount.dataset.smeditorStylesheet;
453
+ if (stylesheet) {
454
+ const link = document.createElement("link");
455
+ link.rel = "stylesheet";
456
+ link.href = stylesheet;
457
+ root.appendChild(link);
458
+ }
459
+ return root;
460
+ }
223
461
  function bootMount(mount) {
462
+ var _a, _b;
224
463
  if (instances.has(mount))
225
464
  return;
226
- if (mount.smeditorInstance) {
227
- instances.set(mount, mount.smeditorInstance);
228
- return;
229
- }
230
465
  const field = mount.closest(".smeditor-field");
231
466
  const input = field === null || field === void 0 ? void 0 : field.querySelector("[data-smeditor-input]");
232
467
  if (!field || !input)
@@ -234,14 +469,18 @@ function bootMount(mount) {
234
469
  const kit = mount.dataset.smeditorKit === "full" ? "full" : "starter";
235
470
  const uploadUrl = mount.dataset.smeditorUploadUrl || undefined;
236
471
  const placeholder = mount.dataset.smeditorPlaceholder || undefined;
237
- mount.innerHTML = "";
472
+ const explicitTheme = ((_b = (_a = mount.parentElement) === null || _a === void 0 ? void 0 : _a.closest("[data-theme]")) === null || _b === void 0 ? void 0 : _b.dataset.theme)
473
+ || document.documentElement.dataset.theme;
474
+ if (explicitTheme === "light" || explicitTheme === "dark")
475
+ mount.dataset.theme = explicitTheme;
476
+ const root = shadowRootFor(mount);
238
477
  const shell = document.createElement("div");
239
478
  shell.className = "smeditor";
240
479
  const surface = document.createElement("div");
241
480
  surface.className = "smeditor-editor";
242
481
  surface.setAttribute("aria-label", mount.dataset.smeditorLabel || "Rich text editor");
243
482
  shell.appendChild(surface);
244
- mount.appendChild(shell);
483
+ root.appendChild(shell);
245
484
  const editor = (0, core_1.createEditor)({
246
485
  element: surface,
247
486
  extensions: kitExtensions(kit, uploadUrl),
@@ -254,11 +493,13 @@ function bootMount(mount) {
254
493
  input.dispatchEvent(new CustomEvent("smeditor:change", { bubbles: true, detail: { editor: current } }));
255
494
  },
256
495
  });
257
- shell.insertBefore(buildToolbar(editor, kit, uploadUrl), surface);
496
+ const toolbar = buildToolbar(editor, kit, uploadUrl);
497
+ shell.insertBefore(toolbar, surface);
258
498
  input.smeditorInstance = editor;
259
499
  mount.smeditorInstance = editor;
260
500
  mount.dataset.smeditorBooted = "1";
261
501
  instances.set(mount, editor);
502
+ disposers.set(mount, [() => { var _a, _b; return (_b = (_a = toolbar).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a); }]);
262
503
  }
263
504
  function boot(root = document) {
264
505
  root.querySelectorAll("[data-smeditor]").forEach(bootMount);
@@ -269,12 +510,9 @@ function destroy(root = document) {
269
510
  const editor = instances.get(mount);
270
511
  if (!editor)
271
512
  return;
272
- const surface = editor.element;
273
- if (surface) {
274
- for (const dispose of (_a = disposers.get(surface)) !== null && _a !== void 0 ? _a : [])
275
- dispose();
276
- disposers.delete(surface);
277
- }
513
+ for (const dispose of (_a = disposers.get(mount)) !== null && _a !== void 0 ? _a : [])
514
+ dispose();
515
+ disposers.delete(mount);
278
516
  editor.destroy();
279
517
  const field = mount.closest(".smeditor-field");
280
518
  const input = field === null || field === void 0 ? void 0 : field.querySelector("[data-smeditor-input]");
@@ -283,7 +521,8 @@ function destroy(root = document) {
283
521
  delete mount.smeditorInstance;
284
522
  delete mount.dataset.smeditorBooted;
285
523
  instances.delete(mount);
286
- mount.innerHTML = "";
524
+ if (mount.shadowRoot)
525
+ mount.shadowRoot.innerHTML = "";
287
526
  });
288
527
  }
289
528
  const api = { boot, destroy, createEditor: core_1.createEditor, StarterKit: starter_kit_1.StarterKit, FullKit: full_kit_1.FullKit, ImageExtension: starter_kit_1.ImageExtension };
@@ -291,12 +530,10 @@ if (typeof window !== "undefined") {
291
530
  window.SMEditor = Object.assign(window.SMEditor || {}, api);
292
531
  if (!window.__smeditorRailsEventsBound) {
293
532
  window.__smeditorRailsEventsBound = true;
294
- if (document.readyState === "loading") {
533
+ if (document.readyState === "loading")
295
534
  document.addEventListener("DOMContentLoaded", () => { var _a; return (_a = window.SMEditor) === null || _a === void 0 ? void 0 : _a.boot(); });
296
- }
297
- else {
535
+ else
298
536
  boot();
299
- }
300
537
  document.addEventListener("turbo:load", () => { var _a; return (_a = window.SMEditor) === null || _a === void 0 ? void 0 : _a.boot(); });
301
538
  document.addEventListener("turbo:frame-load", (event) => { var _a; return (_a = window.SMEditor) === null || _a === void 0 ? void 0 : _a.boot(event.target || document); });
302
539
  document.addEventListener("turbo:before-cache", () => { var _a; return (_a = window.SMEditor) === null || _a === void 0 ? void 0 : _a.destroy(); });
@@ -526,14 +763,14 @@ class Editor {
526
763
  */
527
764
  coordsAtPoint(point) {
528
765
  var _a;
529
- if (!this.element || typeof document === "undefined")
766
+ if (!this.element)
530
767
  return null;
531
768
  const deep = (_a = this.options.deepSelection) !== null && _a !== void 0 ? _a : false;
532
769
  const loc = domNodeFromPoint(point, this.element, deep);
533
770
  if (!loc)
534
771
  return null;
535
772
  try {
536
- const range = document.createRange();
773
+ const range = this.element.ownerDocument.createRange();
537
774
  const offset = Math.min(loc.offset, lengthOf(loc.node));
538
775
  range.setStart(loc.node, offset);
539
776
  range.collapse(true);
@@ -1006,9 +1243,7 @@ class Editor {
1006
1243
  var _a, _b;
1007
1244
  if (this.updatingDOM || !this.element)
1008
1245
  return;
1009
- if (typeof document === "undefined")
1010
- return;
1011
- const domSel = document.getSelection();
1246
+ const domSel = selectionForElement(this.element);
1012
1247
  if (!domSel)
1013
1248
  return;
1014
1249
  if (!this.element.contains(domSel.anchorNode))
@@ -1030,9 +1265,9 @@ class Editor {
1030
1265
  }
1031
1266
  readSelectionFromDOM() {
1032
1267
  var _a;
1033
- if (!this.element || typeof document === "undefined")
1268
+ if (!this.element)
1034
1269
  return null;
1035
- const sel = document.getSelection();
1270
+ const sel = selectionForElement(this.element);
1036
1271
  if (!sel || sel.rangeCount === 0)
1037
1272
  return null;
1038
1273
  const deep = (_a = this.options.deepSelection) !== null && _a !== void 0 ? _a : false;
@@ -1053,10 +1288,10 @@ class Editor {
1053
1288
  }
1054
1289
  applySelectionToDOM() {
1055
1290
  var _a;
1056
- if (!this.selection || !this.element || typeof document === "undefined") {
1291
+ if (!this.selection || !this.element) {
1057
1292
  return;
1058
1293
  }
1059
- const sel = document.getSelection();
1294
+ const sel = selectionForElement(this.element);
1060
1295
  if (!sel)
1061
1296
  return;
1062
1297
  const deep = (_a = this.options.deepSelection) !== null && _a !== void 0 ? _a : false;
@@ -1065,7 +1300,7 @@ class Editor {
1065
1300
  if (!anchorNode || !headNode)
1066
1301
  return;
1067
1302
  try {
1068
- const range = document.createRange();
1303
+ const range = this.element.ownerDocument.createRange();
1069
1304
  range.setStart(anchorNode.node, Math.min(anchorNode.offset, lengthOf(anchorNode.node)));
1070
1305
  range.setEnd(headNode.node, Math.min(headNode.offset, lengthOf(headNode.node)));
1071
1306
  sel.removeAllRanges();
@@ -1116,6 +1351,30 @@ class Editor {
1116
1351
  // ============================================================================
1117
1352
  // Helpers (module-private)
1118
1353
  // ============================================================================
1354
+ /**
1355
+ * Return the Selection object that actually owns the editor caret.
1356
+ *
1357
+ * `document.getSelection()` is not sufficient for editors mounted inside an
1358
+ * open ShadowRoot: Chromium exposes a composed selection on the document whose
1359
+ * anchor can be the shadow host instead of the text node inside the editor.
1360
+ * Reading that selection produces a null/wrong editor position; the next DOM
1361
+ * render then loses the caret (most visibly when typing spaces).
1362
+ *
1363
+ * Both Document and Chromium's ShadowRoot expose getSelection(). Prefer the
1364
+ * editor's root and fall back to its owner document for browsers that do not
1365
+ * expose ShadowRoot#getSelection.
1366
+ */
1367
+ function selectionForElement(element) {
1368
+ var _a, _b, _c, _d;
1369
+ const root = (_a = element.getRootNode) === null || _a === void 0 ? void 0 : _a.call(element);
1370
+ const rootedGetSelection = root === null || root === void 0 ? void 0 : root.getSelection;
1371
+ if (root && typeof rootedGetSelection === "function") {
1372
+ const selection = rootedGetSelection.call(root);
1373
+ if (selection)
1374
+ return selection;
1375
+ }
1376
+ return (_d = (_c = (_b = element.ownerDocument) === null || _b === void 0 ? void 0 : _b.getSelection) === null || _c === void 0 ? void 0 : _c.call(_b)) !== null && _d !== void 0 ? _d : null;
1377
+ }
1119
1378
  /**
1120
1379
  * Strip noise and unsafe elements from pasted HTML.
1121
1380
  *
@@ -17,6 +17,8 @@
17
17
  * light, and inherited by everything not explicitly tagged dark.
18
18
  * ------------------------------------------------------------------------ */
19
19
  :root,
20
+ :host,
21
+ :host([data-theme="light"]),
20
22
  [data-theme="light"] {
21
23
  /* Surface */
22
24
  --sme-color-surface: #ffffff;
@@ -44,7 +46,8 @@
44
46
  }
45
47
 
46
48
  /* Common (non-color) tokens — shared by both themes. */
47
- :root {
49
+ :root,
50
+ :host {
48
51
  /* Spacing */
49
52
  --sme-space-1: 4px;
50
53
  --sme-space-2: 8px;
@@ -82,6 +85,7 @@
82
85
  * Dark theme — applied when explicitly tagged, OR via media query when
83
86
  * no explicit theme is set on the document.
84
87
  * ------------------------------------------------------------------------ */
88
+ :host([data-theme="dark"]),
85
89
  [data-theme="dark"] {
86
90
  --sme-color-surface: #0f1115;
87
91
  --sme-color-surface-muted: #1a1d23;
@@ -105,7 +109,8 @@
105
109
  * `:not([data-theme])` guard means a `data-theme="light"` setting on
106
110
  * any ancestor wins, even at night. */
107
111
  @media (prefers-color-scheme: dark) {
108
- :root:not([data-theme="light"]) {
112
+ :root:not([data-theme="light"]),
113
+ :host(:not([data-theme="light"])) {
109
114
  --sme-color-surface: #0f1115;
110
115
  --sme-color-surface-muted: #1a1d23;
111
116
  --sme-color-border: #2a2d35;
@@ -1210,9 +1215,31 @@
1210
1215
  }
1211
1216
 
1212
1217
 
1213
- /* Rails self-contained adapter controls */
1214
- .smeditor-select { height: 30px; max-width: 130px; border: 1px solid var(--sme-color-border); border-radius: var(--sme-radius-sm); background: var(--sme-color-surface); color: var(--sme-color-text); padding: 0 6px; font: inherit; font-size: var(--sme-text-sm); }
1215
- .smeditor-select:focus-visible, .smeditor-color-input:focus-visible { outline: 2px solid var(--sme-color-accent); outline-offset: 1px; }
1216
- .smeditor-color-input { width: 30px; height: 30px; padding: 2px; border: 1px solid var(--sme-color-border); border-radius: var(--sme-radius-sm); background: var(--sme-color-surface); cursor: pointer; }
1218
+ /* Rails self-contained adapter / style isolation */
1219
+ :host {
1220
+ display: block;
1221
+ width: 100%;
1222
+ color: var(--sme-color-text);
1223
+ font-family: var(--sme-font-sans);
1224
+ font-size: var(--sme-text-base);
1225
+ line-height: normal;
1226
+ text-align: initial;
1227
+ }
1228
+ .smeditor,
1229
+ .smeditor *,
1230
+ .smeditor *::before,
1231
+ .smeditor *::after { box-sizing: border-box; }
1232
+ .smeditor button,
1233
+ .smeditor input,
1234
+ .smeditor select,
1235
+ .smeditor textarea {
1236
+ margin: 0;
1237
+ font: inherit;
1238
+ color: inherit;
1239
+ letter-spacing: normal;
1240
+ text-transform: none;
1241
+ }
1242
+ .smeditor button { -webkit-tap-highlight-color: transparent; }
1243
+ .smeditor svg { display: block; flex: 0 0 auto; }
1217
1244
  .smeditor-file-input { display: none !important; }
1218
1245
  .smeditor-mount { width: 100%; }
@@ -30,6 +30,7 @@ module SMEditor
30
30
  kit = (options[:kit] || SMEditor.config.default_kit).to_s
31
31
  upload_url = options.fetch(:upload_url, smeditor_upload_url)
32
32
  include_assets = options.fetch(:include_assets, SMEditor.config.auto_include_assets)
33
+ stylesheet_url = asset_path("smeditor.css")
33
34
 
34
35
  hidden = form.hidden_field(
35
36
  method,
@@ -47,6 +48,7 @@ module SMEditor
47
48
  smeditor_placeholder: options[:placeholder],
48
49
  smeditor_upload_url: upload_url,
49
50
  smeditor_label: options[:label],
51
+ smeditor_stylesheet: stylesheet_url,
50
52
  }.compact,
51
53
  )
52
54
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SMEditor
4
4
  module Rails
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - SMEditor contributors
@@ -96,7 +96,7 @@ metadata:
96
96
  source_code_uri: https://github.com/sCruze/smeditor/tree/main/gems/smeditor
97
97
  changelog_uri: https://github.com/sCruze/smeditor/blob/main/gems/smeditor/CHANGELOG.md
98
98
  bug_tracker_uri: https://github.com/sCruze/smeditor/issues
99
- documentation_uri: https://rubydoc.info/gems/smeditor/0.2.0
99
+ documentation_uri: https://rubydoc.info/gems/smeditor/0.2.2
100
100
  rubygems_mfa_required: 'true'
101
101
  rdoc_options: []
102
102
  require_paths: