@base44/vite-plugin 1.0.18 → 1.0.20
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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/injections/auto-popup-suppressor.d.ts +6 -0
- package/dist/injections/auto-popup-suppressor.d.ts.map +1 -0
- package/dist/injections/auto-popup-suppressor.js +264 -0
- package/dist/injections/auto-popup-suppressor.js.map +1 -0
- package/dist/injections/visual-edit-agent.d.ts.map +1 -1
- package/dist/injections/visual-edit-agent.js +5 -0
- package/dist/injections/visual-edit-agent.js.map +1 -1
- package/dist/statics/index.mjs +7 -7
- package/dist/statics/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/injections/auto-popup-suppressor.ts +271 -0
- package/src/injections/visual-edit-agent.ts +6 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -57,6 +57,13 @@ export default function vitePlugin(
|
|
|
57
57
|
strictPort: true,
|
|
58
58
|
// Allow all hosts - essential for Modal tunnel URLs
|
|
59
59
|
allowedHosts: true,
|
|
60
|
+
// Route the HMR WebSocket through the preview proxy instead of
|
|
61
|
+
// the sandbox origin. When BASE44_HMR_HOST is unset (e.g. before
|
|
62
|
+
// the proxy is deployed), fall back to Vite's default behavior
|
|
63
|
+
// and connect to window.location.host.
|
|
64
|
+
...(process.env.BASE44_HMR_HOST
|
|
65
|
+
? { hmr: { host: process.env.BASE44_HMR_HOST } }
|
|
66
|
+
: {}),
|
|
60
67
|
watch: {
|
|
61
68
|
// Enable polling for better file change detection in containers
|
|
62
69
|
usePolling: true,
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// auto-popup-suppressor — hides auto-opening modal popups in the canvas
|
|
2
|
+
// preview iframe so the page snapshot isn't obscured by overlays the user
|
|
3
|
+
// didn't intend to capture. Driven by `{ type: "suppress-auto-popups" }` from
|
|
4
|
+
// the parent; inert until that arrives.
|
|
5
|
+
//
|
|
6
|
+
// Two-stage heuristic:
|
|
7
|
+
//
|
|
8
|
+
// 1. BACKDROP MATCH — element must be `position: fixed`, cover ≥90% of the
|
|
9
|
+
// viewport on both axes, have a popup-level z-index, and show backdrop-
|
|
10
|
+
// like styles (translucent background OR backdrop-filter). This is the
|
|
11
|
+
// modal-overlay fingerprint.
|
|
12
|
+
//
|
|
13
|
+
// 2. PANEL SWEEP — once we've hidden any backdrop, sweep the whole document
|
|
14
|
+
// for fixed/high-z elements whose bbox falls in a "modal-shaped" band
|
|
15
|
+
// (15–95% on both axes). Picks up the dialog content panel even when it
|
|
16
|
+
// isn't a direct sibling of the overlay (Radix wraps content in
|
|
17
|
+
// FocusScope + DismissableLayer + focus-guards; shadcn's portals use
|
|
18
|
+
// `asChild`, so structural relationships vary).
|
|
19
|
+
//
|
|
20
|
+
// A sticky `backdropEverDetected` flag keeps the panel-shape check live for
|
|
21
|
+
// later MutationObserver ticks — Content sometimes mounts in a different
|
|
22
|
+
// batch than its Overlay.
|
|
23
|
+
//
|
|
24
|
+
// Why the percentage gates aren't based on window.innerHeight: the canvas
|
|
25
|
+
// iframe is auto-sized to the document content (e.g. 4958px), so innerHeight
|
|
26
|
+
// is the document height, not the visible viewport. page-height-bridge sets
|
|
27
|
+
// `--base44-reference-vh-base` to the canvas tile height — the viewport the
|
|
28
|
+
// user actually sees — and that's what our gates use.
|
|
29
|
+
|
|
30
|
+
const SUPPRESSED_ATTR = "data-base44-suppressed-popup";
|
|
31
|
+
const REFERENCE_VH_BASE_VAR = "--base44-reference-vh-base";
|
|
32
|
+
|
|
33
|
+
const FULL_VIEWPORT_RATIO = 0.9;
|
|
34
|
+
const MODAL_PANEL_MIN_RATIO = 0.15;
|
|
35
|
+
const MODAL_PANEL_MAX_RATIO = 0.95;
|
|
36
|
+
const POPUP_Z_INDEX_THRESHOLD = 40;
|
|
37
|
+
|
|
38
|
+
type Viewport = { vw: number; vh: number };
|
|
39
|
+
|
|
40
|
+
export type AutoPopupSuppressorController = {
|
|
41
|
+
suppress: () => void;
|
|
42
|
+
teardown: () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function createAutoPopupSuppressorController(): AutoPopupSuppressorController {
|
|
48
|
+
let observer: MutationObserver | null = null;
|
|
49
|
+
let domReadyHandler: (() => void) | null = null;
|
|
50
|
+
let active = false;
|
|
51
|
+
// Once a backdrop has been seen, modal-shaped panels appearing in later
|
|
52
|
+
// mutation batches are also hidden. Without this, a Content node that
|
|
53
|
+
// mounts after its Overlay's batch wouldn't be matched in-flight.
|
|
54
|
+
let backdropEverDetected = false;
|
|
55
|
+
|
|
56
|
+
// Single decision per element. One getComputedStyle call — backdrop and
|
|
57
|
+
// panel checks share the position/z-index preconditions. Without sharing,
|
|
58
|
+
// every non-matching node in a React commit's added subtree would pay for
|
|
59
|
+
// two style recalcs, which is the MutationObserver hot path.
|
|
60
|
+
const classify = (el: Element, vp: Viewport): void => {
|
|
61
|
+
if (!(el instanceof HTMLElement)) return;
|
|
62
|
+
if (el.hasAttribute(SUPPRESSED_ATTR)) return;
|
|
63
|
+
const style = window.getComputedStyle(el);
|
|
64
|
+
if (style.position !== "fixed") return;
|
|
65
|
+
if (!hasPopupZIndex(style)) return;
|
|
66
|
+
if (coversFullViewport(el, vp) && hasBackdropLook(el, style)) {
|
|
67
|
+
hideBackdrop(el);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (backdropEverDetected && hasModalShapeBbox(el, vp)) {
|
|
71
|
+
applyHide(el);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hideBackdrop = (el: Element): void => {
|
|
76
|
+
if (el.hasAttribute(SUPPRESSED_ATTR)) return;
|
|
77
|
+
applyHide(el);
|
|
78
|
+
backdropEverDetected = true;
|
|
79
|
+
sweepDocumentForModalPanels(viewportForGates());
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const scanSubtree = (root: Element, vp: Viewport): void => {
|
|
83
|
+
classify(root, vp);
|
|
84
|
+
const descendants = root.querySelectorAll<HTMLElement>(
|
|
85
|
+
`*:not([${SUPPRESSED_ATTR}])`,
|
|
86
|
+
);
|
|
87
|
+
for (let i = 0; i < descendants.length; i++) {
|
|
88
|
+
const el = descendants[i];
|
|
89
|
+
if (el) classify(el, vp);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const attachObserver = (): void => {
|
|
94
|
+
if (observer) return;
|
|
95
|
+
// viewportForGates is read once per mutation batch, not per added node —
|
|
96
|
+
// a busy React commit can deliver dozens of added subtrees in one tick.
|
|
97
|
+
observer = new MutationObserver((mutations) => {
|
|
98
|
+
const vp = viewportForGates();
|
|
99
|
+
for (const m of mutations) {
|
|
100
|
+
if (m.type !== "childList") continue;
|
|
101
|
+
for (let i = 0; i < m.addedNodes.length; i++) {
|
|
102
|
+
const node = m.addedNodes[i];
|
|
103
|
+
if (node instanceof Element) scanSubtree(node, vp);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const start = (): void => {
|
|
111
|
+
if (!document.body) return;
|
|
112
|
+
scanSubtree(document.body, viewportForGates());
|
|
113
|
+
attachObserver();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
suppress: (): void => {
|
|
118
|
+
if (active) return;
|
|
119
|
+
active = true;
|
|
120
|
+
if (document.body) {
|
|
121
|
+
start();
|
|
122
|
+
} else {
|
|
123
|
+
domReadyHandler = (): void => {
|
|
124
|
+
domReadyHandler = null;
|
|
125
|
+
start();
|
|
126
|
+
};
|
|
127
|
+
document.addEventListener("DOMContentLoaded", domReadyHandler);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
teardown: (): void => {
|
|
131
|
+
active = false;
|
|
132
|
+
backdropEverDetected = false;
|
|
133
|
+
if (observer) {
|
|
134
|
+
observer.disconnect();
|
|
135
|
+
observer = null;
|
|
136
|
+
}
|
|
137
|
+
if (domReadyHandler) {
|
|
138
|
+
document.removeEventListener("DOMContentLoaded", domReadyHandler);
|
|
139
|
+
domReadyHandler = null;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Document-wide panel sweep ───────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
// Triggered once per backdrop hide. Walks the whole document for unhidden
|
|
148
|
+
// fixed/high-z elements whose bbox falls inside the modal-shape band — picks
|
|
149
|
+
// up content panels regardless of how they're nested or which portal they
|
|
150
|
+
// went through.
|
|
151
|
+
function sweepDocumentForModalPanels(vp: Viewport): void {
|
|
152
|
+
if (!document.body) return;
|
|
153
|
+
if (vp.vw <= 0 || vp.vh <= 0) return;
|
|
154
|
+
const candidates = document.body.querySelectorAll<HTMLElement>(
|
|
155
|
+
`*:not([${SUPPRESSED_ATTR}])`,
|
|
156
|
+
);
|
|
157
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
158
|
+
const el = candidates[i];
|
|
159
|
+
if (el && isModalPanel(el, vp)) applyHide(el);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isModalPanel(el: HTMLElement, vp: Viewport): boolean {
|
|
164
|
+
if (el.hasAttribute(SUPPRESSED_ATTR)) return false;
|
|
165
|
+
const style = window.getComputedStyle(el);
|
|
166
|
+
if (style.position !== "fixed") return false;
|
|
167
|
+
if (!hasPopupZIndex(style)) return false;
|
|
168
|
+
return hasModalShapeBbox(el, vp);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Gate predicates ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function coversFullViewport(el: HTMLElement, vp: Viewport): boolean {
|
|
174
|
+
if (vp.vw <= 0 || vp.vh <= 0) return false;
|
|
175
|
+
const rect = el.getBoundingClientRect();
|
|
176
|
+
return (
|
|
177
|
+
rect.width >= vp.vw * FULL_VIEWPORT_RATIO &&
|
|
178
|
+
rect.height >= vp.vh * FULL_VIEWPORT_RATIO
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// "Modal-shaped" = small enough not to be a sidebar/full-screen overlay
|
|
183
|
+
// (≤95% on both axes), big enough not to be a nav/footer/toast (≥15% on both
|
|
184
|
+
// axes). Calibrated against shadcn Dialog content (≈25–50% × 30–50%), top
|
|
185
|
+
// nav (height fails ≤15%), sidebar (height fails ≥95%), toast (height fails
|
|
186
|
+
// ≤15%).
|
|
187
|
+
function hasModalShapeBbox(el: HTMLElement, vp: Viewport): boolean {
|
|
188
|
+
const rect = el.getBoundingClientRect();
|
|
189
|
+
return (
|
|
190
|
+
rect.width >= vp.vw * MODAL_PANEL_MIN_RATIO &&
|
|
191
|
+
rect.height >= vp.vh * MODAL_PANEL_MIN_RATIO &&
|
|
192
|
+
rect.width <= vp.vw * MODAL_PANEL_MAX_RATIO &&
|
|
193
|
+
rect.height <= vp.vh * MODAL_PANEL_MAX_RATIO
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function hasPopupZIndex(style: CSSStyleDeclaration): boolean {
|
|
198
|
+
const z = parseInt(style.zIndex, 10);
|
|
199
|
+
return Number.isFinite(z) && z >= POPUP_Z_INDEX_THRESHOLD;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// True if the element looks like a modal backdrop: translucent fill OR a
|
|
203
|
+
// backdrop-filter (e.g. blur). Also returns true if the element's own bg is
|
|
204
|
+
// transparent but a fixed/high-z descendant paints the backdrop — covers
|
|
205
|
+
// dialog wrappers that delegate the backdrop to a child element.
|
|
206
|
+
function hasBackdropLook(el: HTMLElement, style: CSSStyleDeclaration): boolean {
|
|
207
|
+
if (isTranslucent(style.backgroundColor)) return true;
|
|
208
|
+
if (style.backdropFilter && style.backdropFilter !== "none") return true;
|
|
209
|
+
const descendants = el.querySelectorAll<HTMLElement>("*");
|
|
210
|
+
for (let i = 0; i < descendants.length; i++) {
|
|
211
|
+
const child = descendants[i];
|
|
212
|
+
if (!child) continue;
|
|
213
|
+
const cs = window.getComputedStyle(child);
|
|
214
|
+
if (cs.position !== "fixed" && cs.position !== "absolute") continue;
|
|
215
|
+
if (!hasPopupZIndex(cs)) continue;
|
|
216
|
+
if (isTranslucent(cs.backgroundColor)) return true;
|
|
217
|
+
if (cs.backdropFilter && cs.backdropFilter !== "none") return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isTranslucent(color: string): boolean {
|
|
223
|
+
const alpha = parseAlpha(color);
|
|
224
|
+
return alpha !== null && alpha > 0 && alpha < 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Extracts the alpha channel from a computed background-color across legacy
|
|
228
|
+
// AND modern color syntaxes:
|
|
229
|
+
//
|
|
230
|
+
// • `rgb(r, g, b)` / `rgba(r, g, b, a)` — legacy comma syntax.
|
|
231
|
+
// • `<func>(... / a)` / `<func>(... / a%)` — modern slash-alpha (oklch,
|
|
232
|
+
// oklab, lch, lab, hsl, hsla, color, modern rgb). Tailwind v4 / shadcn
|
|
233
|
+
// theme tokens compute to these.
|
|
234
|
+
// • A recognized color function call without explicit alpha → 1.
|
|
235
|
+
//
|
|
236
|
+
// Returns null when unrecognized; callers treat null as "not a backdrop".
|
|
237
|
+
function parseAlpha(value: string): number | null {
|
|
238
|
+
if (!value) return null;
|
|
239
|
+
const comma = /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*([0-9.]+)\s*)?\)$/.exec(value);
|
|
240
|
+
if (comma) return comma[1] === undefined ? 1 : parseFloat(comma[1]);
|
|
241
|
+
const slash = /\/\s*([0-9.]+)(%?)\s*\)$/.exec(value);
|
|
242
|
+
if (slash) {
|
|
243
|
+
const n = parseFloat(slash[1]!);
|
|
244
|
+
return slash[2] === "%" ? n / 100 : n;
|
|
245
|
+
}
|
|
246
|
+
if (/^[a-z]+\s*\(/.test(value)) return 1;
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Viewport + DOM ops ──────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
// Viewport dimensions used for percentage gates. Width comes from
|
|
253
|
+
// `innerWidth` (unaffected by canvas auto-fit). Height prefers the reference
|
|
254
|
+
// vh set by page-height-bridge — without that, after the iframe auto-resizes
|
|
255
|
+
// to document content, `innerHeight` is the document height and every modal
|
|
256
|
+
// panel falls below the 15% floor.
|
|
257
|
+
function viewportForGates(): Viewport {
|
|
258
|
+
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
259
|
+
const refStr = document.documentElement.style.getPropertyValue(REFERENCE_VH_BASE_VAR);
|
|
260
|
+
const ref = parseFloat(refStr);
|
|
261
|
+
const vh = Number.isFinite(ref) && ref > 0
|
|
262
|
+
? ref
|
|
263
|
+
: (window.innerHeight || document.documentElement.clientHeight || 0);
|
|
264
|
+
return { vw, vh };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function applyHide(el: Element): void {
|
|
268
|
+
if (el.hasAttribute(SUPPRESSED_ATTR)) return;
|
|
269
|
+
el.setAttribute(SUPPRESSED_ATTR, "1");
|
|
270
|
+
(el as HTMLElement).style.setProperty("display", "none", "important");
|
|
271
|
+
}
|
|
@@ -5,12 +5,14 @@ import { createInlineEditController } from "../capabilities/inline-edit/index.js
|
|
|
5
5
|
import { THEME_FONT_PREVIEW_ID } from "../consts.js";
|
|
6
6
|
import { createCanvasWheelZoomBridgeController } from "./canvas-wheel-zoom-bridge.js";
|
|
7
7
|
import { createPageHeightBridgeController } from "./page-height-bridge.js";
|
|
8
|
+
import { createAutoPopupSuppressorController } from "./auto-popup-suppressor.js";
|
|
8
9
|
|
|
9
10
|
const REPOSITION_DELAY_MS = 50;
|
|
10
11
|
|
|
11
12
|
export function setupVisualEditAgent() {
|
|
12
13
|
const canvasWheelZoomBridge = createCanvasWheelZoomBridgeController();
|
|
13
14
|
const pageHeightBridge = createPageHeightBridgeController();
|
|
15
|
+
const autoPopupSuppressor = createAutoPopupSuppressorController();
|
|
14
16
|
|
|
15
17
|
// State variables (replacing React useState/useRef)
|
|
16
18
|
let isVisualEditMode = false;
|
|
@@ -659,6 +661,10 @@ export function setupVisualEditAgent() {
|
|
|
659
661
|
);
|
|
660
662
|
break;
|
|
661
663
|
|
|
664
|
+
case "suppress-auto-popups":
|
|
665
|
+
autoPopupSuppressor.suppress();
|
|
666
|
+
break;
|
|
667
|
+
|
|
662
668
|
case "toggle-canvas-wheel-zoom-bridge":
|
|
663
669
|
canvasWheelZoomBridge.enable();
|
|
664
670
|
break;
|