@upstart.gg/vite-plugins 0.0.38 → 0.0.40

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 (64) hide show
  1. package/dist/upstart-editor-api.d.ts +79 -0
  2. package/dist/upstart-editor-api.d.ts.map +1 -0
  3. package/dist/upstart-editor-api.js +208 -0
  4. package/dist/upstart-editor-api.js.map +1 -0
  5. package/dist/vite-plugin-upstart-attrs.d.ts +3 -3
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +227 -25
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-branding/plugin.d.ts +17 -0
  10. package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -0
  11. package/dist/vite-plugin-upstart-branding/plugin.js +41 -0
  12. package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -0
  13. package/dist/vite-plugin-upstart-branding/runtime.d.ts +10 -0
  14. package/dist/vite-plugin-upstart-branding/runtime.d.ts.map +1 -0
  15. package/dist/vite-plugin-upstart-branding/runtime.js +118 -0
  16. package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -0
  17. package/dist/vite-plugin-upstart-branding/types.d.ts +14 -0
  18. package/dist/vite-plugin-upstart-branding/types.d.ts.map +1 -0
  19. package/dist/vite-plugin-upstart-branding/types.js +1 -0
  20. package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -3
  21. package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/plugin.js +3 -16
  23. package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
  24. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +25 -11
  25. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  26. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts +5 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +16 -0
  29. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -0
  30. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +1 -1
  31. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  32. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +2 -1
  33. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  34. package/dist/vite-plugin-upstart-editor/runtime/index.js +42 -7
  35. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  36. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +6 -1
  37. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  38. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +423 -129
  39. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  40. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +18 -10
  41. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  42. package/dist/vite-plugin-upstart-theme.d.ts +3 -3
  43. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
  44. package/dist/vite-plugin-upstart-theme.js +1 -3
  45. package/dist/vite-plugin-upstart-theme.js.map +1 -1
  46. package/package.json +12 -4
  47. package/src/tests/upstart-editor-api.test.ts +98 -174
  48. package/src/tests/vite-plugin-upstart-attrs.test.ts +408 -105
  49. package/src/tests/vite-plugin-upstart-branding.test.ts +90 -0
  50. package/src/tests/vite-plugin-upstart-editor.test.ts +1 -2
  51. package/src/upstart-editor-api.ts +90 -29
  52. package/src/vite-plugin-upstart-attrs.ts +376 -38
  53. package/src/vite-plugin-upstart-branding/plugin.ts +59 -0
  54. package/src/vite-plugin-upstart-branding/runtime.ts +128 -0
  55. package/src/vite-plugin-upstart-branding/types.ts +10 -0
  56. package/src/vite-plugin-upstart-editor/plugin.ts +4 -19
  57. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +25 -12
  58. package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +12 -0
  59. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +1 -1
  60. package/src/vite-plugin-upstart-editor/runtime/index.ts +39 -5
  61. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +518 -141
  62. package/src/vite-plugin-upstart-editor/runtime/types.ts +18 -4
  63. package/src/vite-plugin-upstart-theme.ts +0 -3
  64. package/src/vite-plugin-upstart-editor/PLAN.md +0 -1391
@@ -1,11 +1,30 @@
1
1
  import { sendToParent } from "./utils.js";
2
- import { getCurrentMode } from "./index.js";
3
- import { Editor } from "@tiptap/core";
2
+ import { Editor, Extension, Node } from "@tiptap/core";
4
3
  import { BubbleMenu } from "@tiptap/extension-bubble-menu";
5
4
  import Placeholder from "@tiptap/extension-placeholder";
6
5
  import StarterKit from "@tiptap/starter-kit";
7
6
 
8
7
  //#region src/vite-plugin-upstart-editor/runtime/text-editor.ts
8
+ /**
9
+ * Custom Document node for inline elements (e.g. <a>, <span>, headings).
10
+ * Uses `inline*` content instead of the default `block+` to prevent
11
+ * TipTap from wrapping text in <p> tags inside inline elements.
12
+ */
13
+ const InlineDocument = Node.create({
14
+ name: "doc",
15
+ topNode: true,
16
+ content: "inline*"
17
+ });
18
+ /**
19
+ * Remaps Enter to insert a <br> (hard break) instead of creating a new paragraph.
20
+ * Used in inline-rich mode where block nodes are not allowed.
21
+ */
22
+ const EnterHardBreak = Extension.create({
23
+ name: "enterHardBreak",
24
+ addKeyboardShortcuts() {
25
+ return { Enter: () => this.editor.commands.setHardBreak() };
26
+ }
27
+ });
9
28
  const DEFAULT_OPTIONS = {
10
29
  richTextElements: [
11
30
  "p",
@@ -13,8 +32,7 @@ const DEFAULT_OPTIONS = {
13
32
  "article",
14
33
  "section"
15
34
  ],
16
- plainTextElements: [
17
- "button",
35
+ inlineRichTextElements: [
18
36
  "a",
19
37
  "span",
20
38
  "h1",
@@ -22,35 +40,106 @@ const DEFAULT_OPTIONS = {
22
40
  "h3",
23
41
  "h4",
24
42
  "h5",
25
- "h6",
26
- "label"
43
+ "h6"
27
44
  ],
45
+ plainTextElements: ["button", "label"],
28
46
  bubbleMenu: true,
29
47
  placeholder: "Start typing...",
30
48
  autoSaveDelay: 1e3
31
49
  };
32
50
  const activeEditors = /* @__PURE__ */ new Map();
33
- const saveTimeouts = /* @__PURE__ */ new Map();
51
+ let i18nSyncInProgress = false;
34
52
  const styleCache = /* @__PURE__ */ new WeakMap();
35
- let isInitialized = false;
53
+ let resolvedOptions = DEFAULT_OPTIONS;
36
54
  let shortcutsRegistered = false;
55
+ let domObserver = null;
56
+ let reactivateTimer = null;
37
57
  /**
38
58
  * Initialize TipTap text editing for elements marked as editable.
59
+ * Activation is deferred to avoid conflicting with React hydration.
39
60
  */
40
61
  function initTextEditor(options = {}) {
41
- if (typeof window === "undefined") return;
42
- if (isInitialized) return;
43
- try {
44
- const resolvedOptions = {
45
- ...DEFAULT_OPTIONS,
46
- ...options
62
+ if (typeof window === "undefined") {
63
+ console.warn("[Upstart Editor] Cannot initialize text editor: not running in a browser environment");
64
+ return;
65
+ }
66
+ resolvedOptions = {
67
+ ...DEFAULT_OPTIONS,
68
+ ...options
69
+ };
70
+ registerKeyboardShortcuts();
71
+ waitForHydration(() => {
72
+ injectEditorStyles();
73
+ startDomObserver();
74
+ });
75
+ }
76
+ function waitForHydration(callback) {
77
+ const STABILITY_MS = 200;
78
+ const onReady = () => {
79
+ let timer = null;
80
+ const settle = () => {
81
+ if (timer) clearTimeout(timer);
82
+ timer = window.setTimeout(() => {
83
+ observer.disconnect();
84
+ callback();
85
+ }, STABILITY_MS);
47
86
  };
87
+ const observer = new MutationObserver(settle);
88
+ observer.observe(document.documentElement, {
89
+ childList: true,
90
+ subtree: true
91
+ });
92
+ settle();
93
+ };
94
+ if (document.readyState === "complete") onReady();
95
+ else window.addEventListener("load", onReady, { once: true });
96
+ }
97
+ /**
98
+ * Inject CSS rules that visually hide original content when an editor is active.
99
+ * This avoids moving DOM nodes (which breaks React hydration).
100
+ *
101
+ * - `font-size: 0` + `color: transparent` hides direct text nodes
102
+ * - `> *:not(.ProseMirror)` hides child elements
103
+ * - ProseMirror gets explicit inline styles to restore text rendering
104
+ */
105
+ function injectEditorStyles() {
106
+ if (document.getElementById("upstart-editor-styles")) return;
107
+ const style = document.createElement("style");
108
+ style.id = "upstart-editor-styles";
109
+ style.textContent = [
110
+ "[data-upstart-editor-active] {",
111
+ " cursor: text;",
112
+ " transition: outline 100ms;",
113
+ "}",
114
+ "[data-upstart-editor-active]:hover {",
115
+ " outline: 1px solid #7270c6;",
116
+ " outline-offset: 4px;",
117
+ "}",
118
+ "[data-upstart-editor-active] .ProseMirror {",
119
+ " outline: none !important;",
120
+ " font-size: inherit !important;",
121
+ " line-height: inherit !important;",
122
+ " color: inherit !important;",
123
+ " letter-spacing: inherit !important;",
124
+ " font-weight: inherit !important;",
125
+ " white-space: inherit !important;",
126
+ "}",
127
+ "[data-upstart-editor-active] .ProseMirror:focus {",
128
+ " outline: none !important;",
129
+ "}"
130
+ ].join("\n");
131
+ document.head.appendChild(style);
132
+ }
133
+ /**
134
+ * Activate editors on all editable elements. Safe to call multiple times.
135
+ */
136
+ function activateAllEditors() {
137
+ try {
138
+ cleanupOrphanedEditors();
48
139
  document.querySelectorAll("[data-upstart-editable-text=\"true\"]").forEach((element) => setupEditableElement(element, resolvedOptions));
49
- registerKeyboardShortcuts();
50
- isInitialized = true;
51
- console.log("[Upstart Editor] Text editor initialized");
140
+ console.log("[Upstart Editor] Text editors activated");
52
141
  } catch (error) {
53
- console.error("[Upstart Editor] Failed to initialize text editor:", error);
142
+ console.error("[Upstart Editor] Failed to activate text editors:", error);
54
143
  sendToParent({
55
144
  type: "editor-error",
56
145
  error: error instanceof Error ? error.message : "Unknown error"
@@ -61,118 +150,190 @@ function initTextEditor(options = {}) {
61
150
  * Destroy all active editors.
62
151
  */
63
152
  function destroyAllActiveEditors() {
153
+ stopDomObserver();
64
154
  for (const hash of activeEditors.keys()) destroyEditor(hash);
65
155
  }
66
156
  function setupEditableElement(element, options) {
67
157
  const hash = getEditableHash(element);
68
- if (!hash) return;
158
+ if (!hash || activeEditors.has(hash)) return;
69
159
  cacheStyles(element);
70
- element.style.transition = "outline 0.15s ease";
71
- element.addEventListener("dblclick", (event) => {
72
- if (getCurrentMode() !== "edit") return;
73
- event.preventDefault();
74
- event.stopPropagation();
75
- if (activeEditors.has(hash)) return;
76
- activateEditor(element, hash, options);
77
- });
78
- element.addEventListener("mouseenter", () => {
79
- if (getCurrentMode() !== "edit") return;
80
- if (!activeEditors.has(hash)) applyHoverStyles(element);
81
- });
82
- element.addEventListener("mouseleave", () => {
83
- if (!activeEditors.has(hash)) restoreStyles(element);
84
- });
160
+ activateEditor(element, hash, options);
85
161
  }
86
162
  function activateEditor(element, hash, options) {
87
- const editor = shouldUseRichText(element, options) ? createRichTextEditor(element, hash, options) : createPlainTextEditor(element, hash, options);
163
+ const mode = getEditorMode(element, options);
164
+ let editor;
165
+ switch (mode) {
166
+ case "block-rich":
167
+ editor = createRichTextEditor(element, hash, options);
168
+ break;
169
+ case "inline-rich":
170
+ editor = createInlineRichTextEditor(element, hash, options);
171
+ break;
172
+ case "plain":
173
+ editor = createPlainTextEditor(element, hash, options);
174
+ break;
175
+ }
176
+ element.dataset.upstartEditorActive = "true";
88
177
  applyActiveStyles(element);
89
178
  activeEditors.set(hash, {
90
179
  editor,
91
180
  element,
92
- hash
181
+ hash,
182
+ mode
93
183
  });
94
184
  }
95
185
  function createPlainTextEditor(element, hash, options) {
186
+ const content = element.textContent ?? "";
187
+ let hasChanged = false;
96
188
  return new Editor({
97
189
  element,
98
- extensions: [StarterKit.configure({
99
- heading: false,
100
- bold: false,
101
- italic: false,
102
- strike: false,
103
- blockquote: false,
104
- bulletList: false,
105
- orderedList: false,
106
- listItem: false,
107
- codeBlock: false,
108
- horizontalRule: false
109
- }), Placeholder.configure({ placeholder: "Click to edit..." })],
110
- content: element.textContent ?? "",
111
- editorProps: { attributes: {
112
- class: "upstart-editor-active",
113
- style: "outline: 2px solid #3b82f6; outline-offset: 2px;"
114
- } },
115
- onUpdate: ({ editor }) => {
116
- debouncedSave(hash, editor.getText(), options.autoSaveDelay);
117
- },
118
- onBlur: ({ editor }) => {
119
- saveText(hash, editor.getText());
120
- destroyEditor(hash);
190
+ extensions: [
191
+ InlineDocument,
192
+ StarterKit.configure({
193
+ document: false,
194
+ heading: false,
195
+ bold: false,
196
+ italic: false,
197
+ strike: false,
198
+ blockquote: false,
199
+ bulletList: false,
200
+ orderedList: false,
201
+ listItem: false,
202
+ codeBlock: false,
203
+ horizontalRule: false
204
+ }),
205
+ Placeholder.configure({ placeholder: "Click to edit..." })
206
+ ],
207
+ content,
208
+ editorProps: { attributes: { class: "upstart-editor-active" } },
209
+ onUpdate: ({ editor: e }) => {
210
+ if (i18nSyncInProgress) return;
211
+ hasChanged = true;
212
+ syncI18nSiblings(hash);
121
213
  },
122
- onCreate: ({ editor }) => {
123
- editor.commands.focus("end");
214
+ onBlur: ({ editor: e }) => {
215
+ if (!hasChanged) return;
216
+ hasChanged = false;
217
+ saveText(hash, e.getText());
124
218
  }
125
219
  });
126
220
  }
127
221
  function createRichTextEditor(element, hash, options) {
222
+ const content = element.innerHTML;
223
+ element.innerHTML = "";
224
+ let hasChanged = false;
128
225
  const bubbleMenuElement = createBubbleMenuElement();
129
226
  const extensions = [StarterKit, Placeholder.configure({ placeholder: options.placeholder })];
130
227
  if (options.bubbleMenu) extensions.push(BubbleMenu.configure({ element: bubbleMenuElement }));
131
- return new Editor({
228
+ const editor = new Editor({
132
229
  element,
133
230
  extensions,
134
- content: element.innerHTML,
135
- editorProps: { attributes: {
136
- class: "upstart-editor-active",
137
- style: "outline: 2px solid #3b82f6; outline-offset: 2px;"
138
- } },
139
- onUpdate: ({ editor: updatedEditor }) => {
140
- debouncedSave(hash, updatedEditor.getHTML(), options.autoSaveDelay);
231
+ content,
232
+ editorProps: { attributes: { class: "upstart-editor-active" } },
233
+ onUpdate: ({ editor: e }) => {
234
+ if (i18nSyncInProgress) return;
235
+ hasChanged = true;
236
+ syncI18nSiblings(hash);
141
237
  },
142
- onBlur: ({ editor: updatedEditor }) => {
143
- saveText(hash, updatedEditor.getHTML());
144
- destroyEditor(hash);
238
+ onBlur: ({ editor: e }) => {
239
+ if (!hasChanged) return;
240
+ hasChanged = false;
241
+ saveText(hash, e.getHTML());
242
+ }
243
+ });
244
+ if (options.bubbleMenu) wireBubbleMenu(bubbleMenuElement, editor);
245
+ return editor;
246
+ }
247
+ function createInlineRichTextEditor(element, hash, options) {
248
+ const content = element.innerHTML;
249
+ element.innerHTML = "";
250
+ let hasChanged = false;
251
+ const bubbleMenuElement = createBubbleMenuElement("inline");
252
+ const extensions = [
253
+ InlineDocument,
254
+ EnterHardBreak,
255
+ StarterKit.configure({
256
+ document: false,
257
+ heading: false,
258
+ blockquote: false,
259
+ bulletList: false,
260
+ orderedList: false,
261
+ listItem: false,
262
+ codeBlock: false,
263
+ horizontalRule: false
264
+ }),
265
+ Placeholder.configure({ placeholder: "Click to edit..." })
266
+ ];
267
+ if (options.bubbleMenu) extensions.push(BubbleMenu.configure({ element: bubbleMenuElement }));
268
+ const editor = new Editor({
269
+ element,
270
+ extensions,
271
+ content,
272
+ editorProps: { attributes: { class: "upstart-editor-active" } },
273
+ onUpdate: ({ editor: e }) => {
274
+ if (i18nSyncInProgress) return;
275
+ hasChanged = true;
276
+ syncI18nSiblings(hash);
145
277
  },
146
- onCreate: ({ editor: createdEditor }) => {
147
- if (options.bubbleMenu) wireBubbleMenu(bubbleMenuElement, createdEditor);
148
- createdEditor.commands.focus("end");
278
+ onBlur: ({ editor: e }) => {
279
+ if (!hasChanged) return;
280
+ hasChanged = false;
281
+ saveText(hash, e.getHTML());
149
282
  }
150
283
  });
284
+ if (options.bubbleMenu) wireBubbleMenu(bubbleMenuElement, editor);
285
+ return editor;
151
286
  }
152
- function shouldUseRichText(element, options) {
287
+ function getEditorMode(element, options) {
153
288
  const tagName = element.tagName.toLowerCase();
154
- if (options.plainTextElements.includes(tagName)) return false;
155
- if (options.richTextElements.includes(tagName)) return true;
156
- return true;
289
+ if (options.plainTextElements.includes(tagName)) return "plain";
290
+ if (options.inlineRichTextElements.includes(tagName)) return "inline-rich";
291
+ if (options.richTextElements.includes(tagName)) return "block-rich";
292
+ return "block-rich";
157
293
  }
158
- function debouncedSave(hash, content, delay) {
159
- const existingTimeout = saveTimeouts.get(hash);
160
- if (existingTimeout) clearTimeout(existingTimeout);
161
- const timeoutId = window.setTimeout(() => {
162
- sendToParent({
163
- type: "text-update",
164
- hash,
165
- newText: content
166
- });
167
- }, delay);
168
- saveTimeouts.set(hash, timeoutId);
294
+ function syncI18nSiblings(sourceHash) {
295
+ console.log("[Upstart Editor] Syncing i18n siblings for", sourceHash);
296
+ const instance = activeEditors.get(sourceHash);
297
+ if (!instance) {
298
+ console.warn("[Upstart Editor] No editor instance found for hash:", sourceHash);
299
+ return;
300
+ }
301
+ const i18nKey = instance.element.dataset.upstartI18n;
302
+ if (!i18nKey) {
303
+ console.warn("[Upstart Editor] No sibling i18n key found for element:", instance.element);
304
+ return;
305
+ }
306
+ const siblings = document.querySelectorAll(`[data-upstart-i18n="${CSS.escape(i18nKey)}"]`);
307
+ console.log(`[Upstart Editor] Found ${siblings.length} sibling(s) with i18n key "${i18nKey}"`);
308
+ const plainContent = instance.editor.getText();
309
+ i18nSyncInProgress = true;
310
+ try {
311
+ for (const sibling of siblings) {
312
+ if (sibling === instance.element) continue;
313
+ const siblingHash = sibling.dataset.upstartHash;
314
+ const siblingInstance = siblingHash ? activeEditors.get(siblingHash) : null;
315
+ if (siblingInstance) {
316
+ console.log(`[Upstart Editor] Updating sibling editor (hash: ${siblingHash}) with new content`, siblingInstance);
317
+ sibling.innerText = plainContent;
318
+ siblingInstance.editor.chain().selectAll().insertContent(plainContent).run();
319
+ } else sibling.textContent = plainContent;
320
+ }
321
+ } finally {
322
+ i18nSyncInProgress = false;
323
+ }
169
324
  }
170
325
  function saveText(hash, newText) {
171
326
  try {
327
+ const [namespace, key] = (activeEditors.get(hash)?.element.dataset ?? {}).upstartI18n?.split(":") ?? [];
172
328
  sendToParent({
173
- type: "text-save",
174
- hash,
175
- newText
329
+ type: "text-edit",
330
+ payload: {
331
+ action: "editText",
332
+ content: newText,
333
+ namespace,
334
+ key,
335
+ language: document.documentElement.lang
336
+ }
176
337
  });
177
338
  console.log("[Upstart Editor] Text save message sent:", hash);
178
339
  } catch (error) {
@@ -186,13 +347,66 @@ function saveText(hash, newText) {
186
347
  function destroyEditor(hash) {
187
348
  const instance = activeEditors.get(hash);
188
349
  if (!instance) return;
350
+ const finalContent = instance.mode === "plain" ? instance.editor.getText() : instance.editor.getHTML();
189
351
  instance.editor.destroy();
352
+ const editorDom = instance.element.querySelector(".ProseMirror");
353
+ if (editorDom) editorDom.remove();
354
+ delete instance.element.dataset.upstartEditorActive;
355
+ if (instance.mode === "plain") instance.element.textContent = finalContent;
356
+ else instance.element.innerHTML = finalContent;
190
357
  restoreStyles(instance.element);
191
358
  activeEditors.delete(hash);
192
- const existingTimeout = saveTimeouts.get(hash);
193
- if (existingTimeout) {
194
- clearTimeout(existingTimeout);
195
- saveTimeouts.delete(hash);
359
+ }
360
+ function startDomObserver() {
361
+ if (domObserver) return;
362
+ domObserver = new MutationObserver((mutations) => {
363
+ let needsReactivation = false;
364
+ for (const mutation of mutations) {
365
+ if (mutation.type !== "childList") continue;
366
+ for (const node of mutation.addedNodes) if (node instanceof HTMLElement && (node.matches?.("[data-upstart-editable-text=\"true\"]") || node.querySelector?.("[data-upstart-editable-text=\"true\"]"))) {
367
+ needsReactivation = true;
368
+ break;
369
+ }
370
+ if (!needsReactivation) {
371
+ for (const node of mutation.removedNodes) if (node instanceof HTMLElement && (node.matches?.("[data-upstart-editable-text=\"true\"]") || node.querySelector?.("[data-upstart-editable-text=\"true\"]"))) {
372
+ needsReactivation = true;
373
+ break;
374
+ }
375
+ }
376
+ if (needsReactivation) break;
377
+ }
378
+ if (needsReactivation) scheduleReactivation();
379
+ });
380
+ domObserver.observe(document.body, {
381
+ childList: true,
382
+ subtree: true
383
+ });
384
+ }
385
+ function stopDomObserver() {
386
+ if (domObserver) {
387
+ domObserver.disconnect();
388
+ domObserver = null;
389
+ }
390
+ if (reactivateTimer) {
391
+ clearTimeout(reactivateTimer);
392
+ reactivateTimer = null;
393
+ }
394
+ }
395
+ function scheduleReactivation() {
396
+ if (reactivateTimer) clearTimeout(reactivateTimer);
397
+ reactivateTimer = window.setTimeout(() => {
398
+ reactivateTimer = null;
399
+ activateAllEditors();
400
+ }, 50);
401
+ }
402
+ /**
403
+ * Remove editors whose host element has been detached from the document
404
+ * (e.g. React replaced the subtree during a re-render).
405
+ */
406
+ function cleanupOrphanedEditors() {
407
+ for (const [hash, instance] of activeEditors) if (!document.contains(instance.element)) {
408
+ instance.editor.destroy();
409
+ activeEditors.delete(hash);
196
410
  }
197
411
  }
198
412
  function registerKeyboardShortcuts() {
@@ -220,17 +434,8 @@ function cacheStyles(element) {
220
434
  transition: element.style.transition || ""
221
435
  });
222
436
  }
223
- function applyHoverStyles(element) {
224
- cacheStyles(element);
225
- element.style.outline = "1px dashed #3b82f6";
226
- element.style.outlineOffset = "2px";
227
- element.style.cursor = "text";
228
- }
229
437
  function applyActiveStyles(element) {
230
438
  cacheStyles(element);
231
- element.style.outline = "2px solid #3b82f6";
232
- element.style.outlineOffset = "2px";
233
- element.style.cursor = "text";
234
439
  }
235
440
  function restoreStyles(element) {
236
441
  const cached = styleCache.get(element);
@@ -246,31 +451,82 @@ function restoreStyles(element) {
246
451
  element.style.cursor = cached.cursor;
247
452
  element.style.transition = cached.transition;
248
453
  }
249
- function createBubbleMenuElement() {
250
- const menu = document.createElement("div");
251
- menu.className = "upstart-editor-bubble-menu";
252
- menu.style.cssText = "display: flex; gap: 6px; padding: 6px 8px; background: #111827; color: #ffffff; border-radius: 6px; font-size: 12px; box-shadow: 0 6px 16px rgba(0,0,0,0.2);";
253
- for (const { label, command } of [
454
+ const BUBBLE_MENU_GROUPS = [
455
+ [
456
+ {
457
+ command: "bold",
458
+ icon: "format-bold",
459
+ title: "Bold"
460
+ },
254
461
  {
255
- label: "B",
256
- command: "bold"
462
+ command: "italic",
463
+ icon: "format-italic",
464
+ title: "Italic"
257
465
  },
258
466
  {
259
- label: "I",
260
- command: "italic"
467
+ command: "strike",
468
+ icon: "format-strikethrough",
469
+ title: "Strikethrough"
261
470
  },
262
471
  {
263
- label: "S",
264
- command: "strike"
472
+ command: "code",
473
+ icon: "code-tags",
474
+ title: "Inline Code"
265
475
  }
266
- ]) {
267
- const button = document.createElement("button");
268
- button.type = "button";
269
- button.textContent = label;
270
- button.dataset.command = command;
271
- button.style.cssText = "background: transparent; border: none; color: inherit; cursor: pointer; font-weight: 600;";
272
- menu.appendChild(button);
273
- }
476
+ ],
477
+ [{
478
+ command: "heading",
479
+ icon: "format-header-1",
480
+ title: "Heading"
481
+ }, {
482
+ command: "blockquote",
483
+ icon: "format-quote-close",
484
+ title: "Blockquote"
485
+ }],
486
+ [{
487
+ command: "bulletList",
488
+ icon: "format-list-bulleted",
489
+ title: "Bullet List"
490
+ }, {
491
+ command: "orderedList",
492
+ icon: "format-list-numbered",
493
+ title: "Ordered List"
494
+ }]
495
+ ];
496
+ function getIconifyUrl(iconName) {
497
+ return `https://api.iconify.design/mdi/${iconName}.svg?height=none&color=%23fff`;
498
+ }
499
+ function createBubbleMenuElement(mode = "block") {
500
+ const groups = mode === "inline" ? [BUBBLE_MENU_GROUPS[0]] : BUBBLE_MENU_GROUPS;
501
+ const menu = document.createElement("div");
502
+ menu.className = "upstart-editor-bubble-menu";
503
+ menu.style.cssText = "display: flex; align-items: center; gap: 2px; padding: 4px; background: #111827; color: #ffffff; border-radius: 8px; font-size: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.25);";
504
+ groups.forEach((group, groupIndex) => {
505
+ if (groupIndex > 0) {
506
+ const separator = document.createElement("div");
507
+ separator.style.cssText = "width: 1px; height: 16px; background: rgba(255,255,255,0.2); margin: 0 4px; flex-shrink: 0;";
508
+ menu.appendChild(separator);
509
+ }
510
+ for (const { command, icon, title } of group) {
511
+ const button = document.createElement("button");
512
+ button.type = "button";
513
+ button.dataset.command = command;
514
+ button.title = title;
515
+ button.style.cssText = "background: transparent; border: none; color: inherit; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 4px; border-radius: 4px; transition: background 0.15s ease;";
516
+ const img = document.createElement("img");
517
+ img.src = getIconifyUrl(icon);
518
+ img.alt = title;
519
+ img.style.cssText = "width: 18px; height: 18px; display: block; pointer-events: none;";
520
+ button.appendChild(img);
521
+ button.addEventListener("mouseenter", () => {
522
+ if (!button.dataset.active) button.style.background = "rgba(255,255,255,0.12)";
523
+ });
524
+ button.addEventListener("mouseleave", () => {
525
+ button.style.background = button.dataset.active ? "rgba(255,255,255,0.2)" : "transparent";
526
+ });
527
+ menu.appendChild(button);
528
+ }
529
+ });
274
530
  return menu;
275
531
  }
276
532
  function wireBubbleMenu(menu, editor) {
@@ -281,12 +537,50 @@ function wireBubbleMenu(menu, editor) {
281
537
  const command = event.target?.dataset.command;
282
538
  if (!command) return;
283
539
  const chain = editor.chain().focus();
284
- if (command === "bold") chain.toggleBold().run();
285
- if (command === "italic") chain.toggleItalic().run();
286
- if (command === "strike") chain.toggleStrike().run();
540
+ switch (command) {
541
+ case "bold":
542
+ chain.toggleBold().run();
543
+ break;
544
+ case "italic":
545
+ chain.toggleItalic().run();
546
+ break;
547
+ case "strike":
548
+ chain.toggleStrike().run();
549
+ break;
550
+ case "code":
551
+ chain.toggleCode().run();
552
+ break;
553
+ case "heading":
554
+ chain.toggleHeading({ level: 1 }).run();
555
+ break;
556
+ case "blockquote":
557
+ chain.toggleBlockquote().run();
558
+ break;
559
+ case "bulletList":
560
+ chain.toggleBulletList().run();
561
+ break;
562
+ case "orderedList":
563
+ chain.toggleOrderedList().run();
564
+ break;
565
+ }
287
566
  });
567
+ const updateActiveStates = () => {
568
+ const buttons = menu.querySelectorAll("button[data-command]");
569
+ for (const button of buttons) {
570
+ const command = button.dataset.command;
571
+ if (command === "heading" ? editor.isActive("heading", { level: 1 }) : editor.isActive(command)) {
572
+ button.dataset.active = "true";
573
+ button.style.background = "rgba(255,255,255,0.2)";
574
+ } else {
575
+ delete button.dataset.active;
576
+ button.style.background = "transparent";
577
+ }
578
+ }
579
+ };
580
+ editor.on("transaction", updateActiveStates);
581
+ updateActiveStates();
288
582
  }
289
583
 
290
584
  //#endregion
291
- export { destroyAllActiveEditors, initTextEditor };
585
+ export { activateAllEditors, destroyAllActiveEditors, initTextEditor };
292
586
  //# sourceMappingURL=text-editor.js.map