smeditor 0.2.0 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd12092236abeb99040e428490e6ef352721d4bb266b0a2933ca22a413350941
4
- data.tar.gz: 8f3e32940dc8fdc7e37a26765d9925b8a0755319714a2d623ea1b8f8fd046091
3
+ metadata.gz: 84ee0bbaa2884f82336e20cf7962934d728d539ab71a68c87c865c09122d1100
4
+ data.tar.gz: 80da9de751d692cb0398e80f9b3017324fbf848873dc6a2e73dfa3c85fe9c8fb
5
5
  SHA512:
6
- metadata.gz: 4fc1cb9a65be6f82544f1be230cb860fd0525df94a401b76921c2664e40a5e4587978cf9d3a2c0d011b230d0a9e7f1b89547cf38063d52ffb8614ef5ad7961b6
7
- data.tar.gz: 4d72b4392ec27c3a13c46510da58a7266dbeeebaef74ef422c9ff7c2b15e7fff4ef499ad3d6cb839f9b31631b334d166af18c2e84f43ec5e22efc8609f4688a1
6
+ metadata.gz: 1a905c9df109f03de095aab6652d2eeedd1d42acffeb505a020872c6ffaf5ad88b4ea3cb7cdf5a518b449528b009c6eb53735ef3d61aa50ad6cc554f19114aeb
7
+ data.tar.gz: eb385c65cbae2a229035f98a66950f160d7d301c475cf9976429eebda3e1cc32faf3acfe3206c3478689f9567d33d78a02554dae6d3fe58f01bd3b7a21dadd9c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog — smeditor
2
2
 
3
+ ## 0.2.1 — 2026-09-09
4
+
5
+ - Rails editor now renders inside Shadow DOM so host application CSS cannot alter the SMEditor toolbar or editing surface.
6
+ - Rails toolbar now uses the same SMEditor visual language and control layout as the playground instead of native selects/text buttons.
7
+ - Theme tokens work inside the isolated editor and continue to follow light/dark preference.
8
+ - The self-contained gem still requires no npm, React, esbuild, or jsbundling-rails in the host Rails application.
9
+
10
+
3
11
  All notable changes to the Rails adapter are recorded here. The format is
4
12
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the gem
5
13
  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(); });
@@ -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.1"
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.1
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.1
100
100
  rubygems_mfa_required: 'true'
101
101
  rdoc_options: []
102
102
  require_paths: