@upstart.gg/vite-plugins 0.1.4 → 0.1.6

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 (62) hide show
  1. package/dist/src/vite-plugin-upstart-editor/runtime/state.d.ts +10 -0
  2. package/dist/src/vite-plugin-upstart-editor/runtime/state.d.ts.map +1 -0
  3. package/dist/src/vite-plugin-upstart-editor/runtime/state.js +18 -0
  4. package/dist/src/vite-plugin-upstart-editor/runtime/state.js.map +1 -0
  5. package/dist/upstart-editor-api.d.ts +0 -3
  6. package/dist/upstart-editor-api.d.ts.map +1 -1
  7. package/dist/upstart-editor-api.js +1 -2
  8. package/dist/upstart-editor-api.js.map +1 -1
  9. package/dist/vite-plugin-upstart-attrs.d.ts +3 -3
  10. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  11. package/dist/vite-plugin-upstart-attrs.js +22 -22
  12. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  13. package/dist/vite-plugin-upstart-branding/plugin.d.ts +3 -4
  14. package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -1
  15. package/dist/vite-plugin-upstart-branding/plugin.js +1 -2
  16. package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -1
  17. package/dist/vite-plugin-upstart-branding/runtime.d.ts.map +1 -1
  18. package/dist/vite-plugin-upstart-branding/runtime.js +2 -2
  19. package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -1
  20. package/dist/vite-plugin-upstart-branding/types.d.ts.map +1 -1
  21. package/dist/vite-plugin-upstart-branding/types.js +1 -1
  22. package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -4
  23. package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
  24. package/dist/vite-plugin-upstart-editor/plugin.js +2 -3
  25. package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
  26. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
  27. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +84 -16
  28. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  29. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -1
  30. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +1 -2
  31. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
  32. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -1
  33. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +78 -21
  34. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  35. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +3 -5
  36. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  37. package/dist/vite-plugin-upstart-editor/runtime/index.js +30 -15
  38. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  39. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +0 -1
  40. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  41. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +9 -37
  42. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  43. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +7 -9
  44. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  45. package/dist/vite-plugin-upstart-editor/runtime/types.js +1 -1
  46. package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts +0 -1
  47. package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts.map +1 -1
  48. package/dist/vite-plugin-upstart-editor/runtime/utils.js +1 -1
  49. package/dist/vite-plugin-upstart-theme.d.ts +3 -3
  50. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
  51. package/dist/vite-plugin-upstart-theme.js +1 -2
  52. package/dist/vite-plugin-upstart-theme.js.map +1 -1
  53. package/package.json +14 -13
  54. package/src/vite-plugin-upstart-attrs.ts +1 -0
  55. package/src/vite-plugin-upstart-branding/runtime.ts +2 -3
  56. package/src/vite-plugin-upstart-editor/plugin.ts +2 -2
  57. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +94 -18
  58. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +94 -20
  59. package/src/vite-plugin-upstart-editor/runtime/index.ts +42 -13
  60. package/src/vite-plugin-upstart-editor/runtime/state.ts +17 -0
  61. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +49 -81
  62. package/src/vite-plugin-upstart-editor/runtime/types.ts +6 -3
@@ -1,8 +1,31 @@
1
- import { getCurrentMode } from "./index.js";
1
+ import { getCurrentMode } from "./state.js";
2
2
  import { sendToParent } from "./utils.js";
3
3
 
4
4
  let isInitialized = false;
5
5
 
6
+ const DAISY_VAR_NAMES = [
7
+ "base-100",
8
+ "base-200",
9
+ "base-300",
10
+ "base-content",
11
+ "primary",
12
+ "primary-content",
13
+ "secondary",
14
+ "secondary-content",
15
+ "accent",
16
+ "accent-content",
17
+ "neutral",
18
+ "neutral-content",
19
+ "info",
20
+ "info-content",
21
+ "success",
22
+ "success-content",
23
+ "warning",
24
+ "warning-content",
25
+ "error",
26
+ "error-content",
27
+ ];
28
+
6
29
  /**
7
30
  * Initialize click handler for className editing.
8
31
  */
@@ -41,6 +64,11 @@ function handleClick(event: MouseEvent): void {
41
64
  return;
42
65
  }
43
66
 
67
+ if (target.closest(".upstart-editor-bubble-menu")) {
68
+ console.info("[Upstart Editor] Click ignored: target is inside the bubble menu");
69
+ return;
70
+ }
71
+
44
72
  const activeLink = target.closest("a[data-upstart-editor-active]");
45
73
  const isFormControlClick = Boolean(
46
74
  (target as HTMLElement | null)?.closest("input, textarea, select, button"),
@@ -49,26 +77,39 @@ function handleClick(event: MouseEvent): void {
49
77
  event.preventDefault();
50
78
  }
51
79
 
80
+ // Clicks inside contenteditable (TipTap active) — still notify style panel if applicable
52
81
  if (target.closest("[contenteditable='true']")) {
53
- console.info("[Upstart Editor] Click ignored: target is inside a contenteditable element");
54
- return;
55
- }
56
-
57
- // Check if the click targets a datasource record (takes priority over class editing)
58
- const datasourceEl = target.closest<HTMLElement>("[data-upstart-datasource][data-upstart-record-id]");
59
- if (datasourceEl) {
60
- event.stopPropagation();
61
- sendToParent({
62
- type: "datasource-item-clicked",
63
- datasourceId: datasourceEl.dataset.upstartDatasource as string,
64
- recordId: datasourceEl.dataset.upstartRecordId as string,
65
- });
82
+ const editableEl = target.closest<HTMLElement>("[data-upstart-editable-text='true']");
83
+ if (editableEl) {
84
+ const classNameId =
85
+ editableEl.dataset.upstartClassnameId ||
86
+ editableEl.closest<HTMLElement>("[data-upstart-classname-id]")?.dataset.upstartClassnameId ||
87
+ "";
88
+ if (classNameId) {
89
+ const computedStyle = getComputedStyle(document.documentElement);
90
+ const themeColors: Record<string, string> = {};
91
+ for (const name of DAISY_VAR_NAMES) {
92
+ const val = computedStyle.getPropertyValue(`--color-${name}`).trim();
93
+ if (val) themeColors[name] = val;
94
+ }
95
+ sendToParent({
96
+ type: "element-clicked",
97
+ hash: editableEl.dataset.upstartHash ?? "",
98
+ componentName: editableEl.dataset.upstartComponent,
99
+ filePath: editableEl.dataset.upstartFile ?? "",
100
+ classNameId,
101
+ currentClassName: editableEl.className,
102
+ themeColors,
103
+ bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },
104
+ });
105
+ }
106
+ }
107
+ console.info("[Upstart Editor] Click inside contenteditable: TipTap handles text editing");
66
108
  return;
67
109
  }
68
110
 
111
+ // Find the closest element with data-upstart-hash
69
112
  const element = target.closest<HTMLElement>("[data-upstart-hash]");
70
- // const i18nKey = target.closest<HTMLElement>("[data-upstart-i18n]");
71
-
72
113
  if (!element) {
73
114
  console.info("[Upstart Editor] Click ignored: no ancestral element with data-upstart-hash found");
74
115
  return;
@@ -81,7 +122,39 @@ function handleClick(event: MouseEvent): void {
81
122
  const hash = element.dataset.upstartHash as string;
82
123
  const componentName = element.dataset.upstartComponent;
83
124
  const filePath = element.dataset.upstartFile ?? "";
84
- const classNameId = element.dataset.upstartClassnameId ?? "";
125
+
126
+ // If this element has editable-text="true", let TipTap handle text editing
127
+ if (element.dataset.upstartEditableText === "true") {
128
+ const proseMirror = element.querySelector<HTMLElement>(".ProseMirror");
129
+ if (proseMirror) {
130
+ proseMirror.focus();
131
+ }
132
+ }
133
+
134
+ // Find classNameId on the element itself, or walk up to find it on a parent
135
+ let classNameId = element.dataset.upstartClassnameId ?? "";
136
+ let currentClassName = element.className;
137
+ if (!classNameId) {
138
+ const parentWithClassname = element.closest<HTMLElement>("[data-upstart-classname-id]");
139
+ if (parentWithClassname) {
140
+ classNameId = parentWithClassname.dataset.upstartClassnameId ?? "";
141
+ currentClassName = parentWithClassname.className;
142
+ }
143
+ }
144
+
145
+ // Check if this element (or a nearby ancestor) is a datasource record
146
+ const datasourceEl = target.closest<HTMLElement>("[data-upstart-datasource][data-upstart-record-id]");
147
+ const datasourceId = datasourceEl?.dataset.upstartDatasource;
148
+ const recordId = datasourceEl?.dataset.upstartRecordId;
149
+
150
+ // Read DaisyUI CSS custom property values from the iframe's document
151
+ const computedStyle = getComputedStyle(document.documentElement);
152
+ const themeColors: Record<string, string> = {};
153
+ for (const name of DAISY_VAR_NAMES) {
154
+ const val = computedStyle.getPropertyValue(`--color-${name}`).trim();
155
+ if (val) themeColors[name] = val;
156
+ }
157
+
85
158
  const rect = element.getBoundingClientRect();
86
159
 
87
160
  sendToParent({
@@ -90,7 +163,10 @@ function handleClick(event: MouseEvent): void {
90
163
  componentName,
91
164
  filePath,
92
165
  classNameId,
93
- currentClassName: element.className,
166
+ currentClassName,
167
+ datasourceId,
168
+ recordId,
169
+ themeColors,
94
170
  bounds: {
95
171
  top: rect.top,
96
172
  left: rect.left,
@@ -1,8 +1,9 @@
1
- import { getCurrentMode } from "./index.js";
1
+ import { getCurrentMode } from "./state.js";
2
2
  import { sendToParent } from "./utils.js";
3
3
 
4
4
  let overlay: HTMLDivElement | null = null;
5
5
  let currentTarget: HTMLElement | null = null;
6
+ let hoveredEl: HTMLElement | null = null;
6
7
  let isInitialized = false;
7
8
  let rafId: number | null = null;
8
9
 
@@ -20,8 +21,35 @@ export function initHoverOverlay(): void {
20
21
 
21
22
  console.log("[Upstart Editor] Initializing hover overlay...");
22
23
 
24
+ const style = document.createElement("style");
25
+ style.id = "upstart-hover-edit-styles";
26
+ style.textContent = [
27
+ ":root[data-upstart-edit-mode] [data-upstart-classname-id]:not([data-upstart-editable-text='true']),",
28
+ ":root[data-upstart-edit-mode] [data-upstart-datasource][data-upstart-record-id]:not([data-upstart-editable-text='true']),",
29
+ ":root[data-upstart-edit-mode] [data-upstart-editor-active] {",
30
+ " transition: outline 150ms, outline-offset 150ms;",
31
+ "}",
32
+ ":root[data-upstart-edit-mode] [data-upstart-hovered] {",
33
+ " outline: 2px solid rgba(114, 112, 198, 0.6);",
34
+ " outline-offset: 4px;",
35
+ " cursor: pointer;",
36
+ "}",
37
+ ":root[data-upstart-edit-mode] [data-upstart-editor-active][data-upstart-hovered] {",
38
+ " outline: 2px solid rgba(114, 112, 198, 0.6);",
39
+ " outline-offset: 4px;",
40
+ " cursor: text;",
41
+ "}",
42
+ ":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor] {",
43
+ " outline: 1px dashed rgba(114, 112, 198, 0.6);",
44
+ " cursor: pointer;",
45
+ "}",
46
+ ].join("\n");
47
+ document.head.appendChild(style);
48
+
23
49
  document.addEventListener("mouseover", handleMouseOver);
24
50
  document.addEventListener("mouseout", handleMouseOut);
51
+ document.addEventListener("keydown", handleKeyChange);
52
+ document.addEventListener("keyup", handleKeyChange);
25
53
  window.addEventListener("scroll", scheduleOverlayUpdate, { passive: true });
26
54
  window.addEventListener("resize", scheduleOverlayUpdate, { passive: true });
27
55
 
@@ -36,6 +64,51 @@ export function hideOverlays(): void {
36
64
  overlay.style.display = "none";
37
65
  currentTarget = null;
38
66
  }
67
+ clearEditableHover();
68
+ }
69
+
70
+ function isEligible(el: HTMLElement): boolean {
71
+ if (el.dataset.upstartEditableText === "true") return false;
72
+ return !!(el.dataset.upstartClassnameId || (el.dataset.upstartDatasource && el.dataset.upstartRecordId));
73
+ }
74
+
75
+ function clearEditableHover(): void {
76
+ for (const el of document.querySelectorAll("[data-upstart-hovered],[data-upstart-hovered-ancestor]")) {
77
+ (el as HTMLElement).removeAttribute("data-upstart-hovered");
78
+ (el as HTMLElement).removeAttribute("data-upstart-hovered-ancestor");
79
+ }
80
+ hoveredEl = null;
81
+ }
82
+
83
+ function applyTextHover(el: HTMLElement): void {
84
+ clearEditableHover();
85
+ el.setAttribute("data-upstart-hovered", "");
86
+ hoveredEl = el;
87
+ }
88
+
89
+ function applyEditableHover(target: HTMLElement, withAncestors: boolean): void {
90
+ clearEditableHover();
91
+
92
+ // Find closest eligible element
93
+ let el: HTMLElement | null = target;
94
+ while (el && el !== document.documentElement) {
95
+ if (isEligible(el)) break;
96
+ el = el.parentElement;
97
+ }
98
+ if (!el || el === document.documentElement) return;
99
+
100
+ el.setAttribute("data-upstart-hovered", "");
101
+ hoveredEl = el;
102
+
103
+ if (withAncestors) {
104
+ let ancestor = el.parentElement;
105
+ while (ancestor && ancestor !== document.documentElement) {
106
+ if (isEligible(ancestor)) {
107
+ ancestor.setAttribute("data-upstart-hovered-ancestor", "");
108
+ }
109
+ ancestor = ancestor.parentElement;
110
+ }
111
+ }
39
112
  }
40
113
 
41
114
  function handleMouseOver(event: MouseEvent): void {
@@ -48,6 +121,18 @@ function handleMouseOver(event: MouseEvent): void {
48
121
  return;
49
122
  }
50
123
 
124
+ // If hovering inside an active text editor, highlight only that element
125
+ // and suppress the component overlay box so parents don't get hover effects.
126
+ const textEl = target.closest<HTMLElement>("[data-upstart-editor-active]");
127
+ if (textEl) {
128
+ applyTextHover(textEl);
129
+ if (overlay) overlay.style.display = "none";
130
+ currentTarget = null;
131
+ return;
132
+ }
133
+
134
+ applyEditableHover(target, event.metaKey || event.ctrlKey);
135
+
51
136
  const component = target.closest<HTMLElement>("[data-upstart-component]");
52
137
  if (!component || component.dataset.upstartEditorActive) {
53
138
  return;
@@ -59,23 +144,6 @@ function handleMouseOver(event: MouseEvent): void {
59
144
 
60
145
  currentTarget = component;
61
146
  positionOverlay(component);
62
-
63
- const hash = component.dataset.upstartHash;
64
- if (hash) {
65
- const rect = component.getBoundingClientRect();
66
- sendToParent({
67
- type: "element-hovered",
68
- hash,
69
- bounds: {
70
- top: rect.top,
71
- left: rect.left,
72
- width: rect.width,
73
- height: rect.height,
74
- right: rect.right,
75
- bottom: rect.bottom,
76
- },
77
- });
78
- }
79
147
  }
80
148
 
81
149
  function handleMouseOut(event: MouseEvent): void {
@@ -98,12 +166,18 @@ function handleMouseOut(event: MouseEvent): void {
98
166
  hideOverlays();
99
167
  }
100
168
 
169
+ function handleKeyChange(event: KeyboardEvent): void {
170
+ if (getCurrentMode() !== "edit" || !hoveredEl) return;
171
+ if (event.key !== "Meta" && event.key !== "Control") return;
172
+ applyEditableHover(hoveredEl, event.type === "keydown" && (event.metaKey || event.ctrlKey));
173
+ }
174
+
101
175
  function createOverlay(): void {
102
176
  overlay = document.createElement("div");
103
177
  overlay.id = "upstart-hover-overlay";
104
178
  overlay.style.cssText =
105
- "position: absolute; pointer-events: none; border: 2px solid #3b82f6; " +
106
- "background: rgba(59, 130, 246, 0.05); border-radius: 4px; z-index: 9999; " +
179
+ "position: absolute; pointer-events: none; border: 2px solid rgba(114, 112, 198, 0.6); " +
180
+ "background: rgba(114, 112, 198, 0.05); border-radius: 4px; z-index: 9999; " +
107
181
  "transition: all 0.1s ease; display: none;";
108
182
  document.body.appendChild(overlay);
109
183
  }
@@ -3,23 +3,18 @@ import { initHoverOverlay, hideOverlays } from "./hover-overlay.js";
3
3
  import { initErrorHandler } from "./error-handler.js";
4
4
  import { initTextEditor, activateAllEditors, destroyAllActiveEditors } from "./text-editor.js";
5
5
  import { sendToParent } from "./utils.js";
6
+ import { getCurrentMode, setCurrentMode } from "./state.js";
6
7
  import type { EditorMode, UpstartParentMessage } from "./types.js";
7
8
 
8
- let currentMode: EditorMode = "preview";
9
9
  let isInitialized = false;
10
10
 
11
- /**
12
- * Get the current editor mode.
13
- */
14
- export function getCurrentMode(): EditorMode {
15
- return currentMode;
16
- }
11
+ export { getCurrentMode };
17
12
 
18
13
  /**
19
14
  * Set the current editor mode.
20
15
  */
21
16
  export function setMode(mode: EditorMode): void {
22
- currentMode = mode;
17
+ setCurrentMode(mode);
23
18
  console.log(`[Upstart Editor] Setting mode to: ${mode}`);
24
19
  if (mode === "edit") {
25
20
  enableEditMode();
@@ -28,15 +23,42 @@ export function setMode(mode: EditorMode): void {
28
23
  }
29
24
  }
30
25
 
26
+ export function waitForHydration(callback: () => void): void {
27
+ // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a
28
+ // concurrent (low-priority) operation that can span many frames after the
29
+ // load event. Instead of guessing a delay, we wait for the DOM to stabilise:
30
+ // once 200 ms pass without any childList mutations, hydration is done.
31
+ const STABILITY_MS = 200;
32
+
33
+ const onReady = () => {
34
+ let timer: number | null = null;
35
+
36
+ const settle = () => {
37
+ if (timer) clearTimeout(timer);
38
+ timer = window.setTimeout(() => {
39
+ observer.disconnect();
40
+ callback();
41
+ }, STABILITY_MS);
42
+ };
43
+
44
+ const observer = new MutationObserver(settle);
45
+ observer.observe(document.documentElement, { childList: true, subtree: true });
46
+
47
+ // Kick off the first timer (covers case where no mutations occur after load)
48
+ settle();
49
+ };
50
+
51
+ if (document.readyState === "complete") {
52
+ onReady();
53
+ } else {
54
+ window.addEventListener("load", onReady, { once: true });
55
+ }
56
+ }
57
+
31
58
  /**
32
59
  * Initialize the Upstart editor runtime.
33
60
  */
34
61
  export function initUpstartEditor(): void {
35
- if (typeof window === "undefined") {
36
- console.warn("[Upstart Editor] Cannot initialize editor: not running in a browser environment");
37
- return;
38
- }
39
-
40
62
  if (isInitialized) {
41
63
  console.log("[Upstart Editor] Editor is already initialized");
42
64
  return;
@@ -105,16 +127,23 @@ function handleParentMessage(event: MessageEvent): void {
105
127
  if (message.type === "set-mode") {
106
128
  console.log("Setting editor mode to:", message.mode);
107
129
  setMode(message.mode);
130
+ } else if (message.type === "preview-classname") {
131
+ const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id="${message.classNameId}"]`);
132
+ if (el) {
133
+ el.className = message.className;
134
+ }
108
135
  }
109
136
  }
110
137
 
111
138
  function enableEditMode(): void {
112
139
  console.log("[Upstart Editor] Edit mode enabled");
140
+ document.documentElement.setAttribute("data-upstart-edit-mode", "");
113
141
  activateAllEditors();
114
142
  }
115
143
 
116
144
  function disableEditMode(): void {
117
145
  console.log("[Upstart Editor] Preview mode enabled");
146
+ document.documentElement.removeAttribute("data-upstart-edit-mode");
118
147
  destroyAllActiveEditors();
119
148
  hideOverlays();
120
149
  }
@@ -0,0 +1,17 @@
1
+ import type { EditorMode } from "./types.js";
2
+
3
+ let currentMode: EditorMode = "preview";
4
+
5
+ /**
6
+ * Get the current editor mode.
7
+ */
8
+ export function getCurrentMode(): EditorMode {
9
+ return currentMode;
10
+ }
11
+
12
+ /**
13
+ * Set the current editor mode (internal use only).
14
+ */
15
+ export function setCurrentMode(mode: EditorMode): void {
16
+ currentMode = mode;
17
+ }
@@ -54,52 +54,13 @@ let reactivateTimer: number | null = null;
54
54
  * Activation is deferred to avoid conflicting with React hydration.
55
55
  */
56
56
  export function initTextEditor(options: UpstartEditorOptions = {}): void {
57
- if (typeof window === "undefined") {
58
- console.warn("[Upstart Editor] Cannot initialize text editor: not running in a browser environment");
59
- return;
60
- }
61
-
62
57
  resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
63
58
  registerKeyboardShortcuts();
64
59
 
65
60
  // Defer activation to let React finish hydrating the server-rendered HTML.
66
- waitForHydration(() => {
67
- injectEditorStyles();
68
- // activateAllEditors();
69
- startDomObserver();
70
- });
71
- }
72
-
73
- function waitForHydration(callback: () => void): void {
74
- // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a
75
- // concurrent (low-priority) operation that can span many frames after the
76
- // load event. Instead of guessing a delay, we wait for the DOM to stabilise:
77
- // once 200 ms pass without any childList mutations, hydration is done.
78
- const STABILITY_MS = 200;
79
-
80
- const onReady = () => {
81
- let timer: number | null = null;
82
-
83
- const settle = () => {
84
- if (timer) clearTimeout(timer);
85
- timer = window.setTimeout(() => {
86
- observer.disconnect();
87
- callback();
88
- }, STABILITY_MS);
89
- };
90
-
91
- const observer = new MutationObserver(settle);
92
- observer.observe(document.documentElement, { childList: true, subtree: true });
93
-
94
- // Kick off the first timer (covers case where no mutations occur after load)
95
- settle();
96
- };
97
-
98
- if (document.readyState === "complete") {
99
- onReady();
100
- } else {
101
- window.addEventListener("load", onReady, { once: true });
102
- }
61
+ injectEditorStyles();
62
+ // activateAllEditors();
63
+ startDomObserver();
103
64
  }
104
65
 
105
66
  /**
@@ -118,11 +79,6 @@ function injectEditorStyles(): void {
118
79
  style.textContent = [
119
80
  "[data-upstart-editor-active] {",
120
81
  " cursor: text;",
121
- " transition: outline 100ms;",
122
- "}",
123
- "[data-upstart-editor-active]:hover {",
124
- " outline: 1px solid #7270c6;",
125
- " outline-offset: 4px;",
126
82
  "}",
127
83
  // "[data-upstart-editor-active] > *:not(.ProseMirror) {",
128
84
  // " display: none !important;",
@@ -212,6 +168,7 @@ function createPlainTextEditor(
212
168
  options: Required<UpstartEditorOptions>,
213
169
  ): Editor {
214
170
  const content = element.textContent ?? "";
171
+ element.textContent = ""; // Clear the element first! (same as rich text editors)
215
172
  let hasChanged = false;
216
173
 
217
174
  const editor = new Editor({
@@ -380,6 +337,11 @@ function createInlineRichTextEditor(
380
337
  }
381
338
 
382
339
  function getEditorMode(element: HTMLElement, options: Required<UpstartEditorOptions>): TextEditorMode {
340
+ const modeOverride = element.dataset.upstartEditableTextMode as TextEditorMode | undefined;
341
+ if (modeOverride === "plain" || modeOverride === "inline-rich" || modeOverride === "block-rich") {
342
+ return modeOverride;
343
+ }
344
+
383
345
  const tagName = element.tagName.toLowerCase();
384
346
 
385
347
  if (options.plainTextElements.includes(tagName)) {
@@ -721,40 +683,46 @@ function wireBubbleMenu(menu: HTMLDivElement, editor: Editor): void {
721
683
  event.preventDefault();
722
684
  });
723
685
 
724
- menu.addEventListener("click", (event) => {
725
- const target = event.target as HTMLElement | null;
726
- const command = target?.dataset.command;
727
- if (!command) return;
728
-
729
- const chain = editor.chain().focus();
730
-
731
- switch (command) {
732
- case "bold":
733
- chain.toggleBold().run();
734
- break;
735
- case "italic":
736
- chain.toggleItalic().run();
737
- break;
738
- case "strike":
739
- chain.toggleStrike().run();
740
- break;
741
- case "code":
742
- chain.toggleCode().run();
743
- break;
744
- case "heading":
745
- chain.toggleHeading({ level: 1 }).run();
746
- break;
747
- case "blockquote":
748
- chain.toggleBlockquote().run();
749
- break;
750
- case "bulletList":
751
- chain.toggleBulletList().run();
752
- break;
753
- case "orderedList":
754
- chain.toggleOrderedList().run();
755
- break;
756
- }
757
- });
686
+ menu.addEventListener(
687
+ "click",
688
+ (event) => {
689
+ console.log("menu button cliked");
690
+ event.stopPropagation();
691
+ const target = event.target as HTMLElement | null;
692
+ const command = target?.dataset.command;
693
+ if (!command) return;
694
+
695
+ const chain = editor.chain().focus();
696
+
697
+ switch (command) {
698
+ case "bold":
699
+ chain.toggleBold().run();
700
+ break;
701
+ case "italic":
702
+ chain.toggleItalic().run();
703
+ break;
704
+ case "strike":
705
+ chain.toggleStrike().run();
706
+ break;
707
+ case "code":
708
+ chain.toggleCode().run();
709
+ break;
710
+ case "heading":
711
+ chain.toggleHeading({ level: 1 }).run();
712
+ break;
713
+ case "blockquote":
714
+ chain.toggleBlockquote().run();
715
+ break;
716
+ case "bulletList":
717
+ chain.toggleBulletList().run();
718
+ break;
719
+ case "orderedList":
720
+ chain.toggleOrderedList().run();
721
+ break;
722
+ }
723
+ },
724
+ { capture: true },
725
+ );
758
726
 
759
727
  const updateActiveStates = (): void => {
760
728
  const buttons = menu.querySelectorAll<HTMLButtonElement>("button[data-command]");
@@ -41,8 +41,6 @@ export type EditorMessage =
41
41
  | { type: "editor-ready" }
42
42
  | { type: "editor-navigated" }
43
43
  | { type: "editor-error"; error: string }
44
- | { type: "element-hovered"; hash: string; bounds: ElementBounds }
45
- | { type: "datasource-item-clicked"; datasourceId: string; recordId: string }
46
44
  | {
47
45
  type: "element-clicked";
48
46
  hash: string;
@@ -50,13 +48,18 @@ export type EditorMessage =
50
48
  filePath: string;
51
49
  currentClassName: string;
52
50
  classNameId: string;
51
+ datasourceId?: string;
52
+ recordId?: string;
53
+ themeColors?: Record<string, string>;
53
54
  bounds: ElementBounds;
54
55
  };
55
56
 
56
57
  /**
57
58
  * Messages sent from parent to iframe.
58
59
  */
59
- export type ParentMessage = { type: "set-mode"; mode: EditorMode };
60
+ export type ParentMessage =
61
+ | { type: "set-mode"; mode: EditorMode }
62
+ | { type: "preview-classname"; classNameId: string; className: string };
60
63
 
61
64
  /**
62
65
  * Message wrapper sent via postMessage from iframe.