@base44/vite-plugin 1.0.12 → 1.0.14

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.
@@ -3,12 +3,13 @@
3
3
  // fire on their own.
4
4
  //
5
5
  // parent → child { type: "freeze-vh-units", referenceVhBase?: number }
6
- // Rewrites every `vh` in <style> + CSSOM to fixed `px` (kills the
7
- // vh ↔ auto-resize feedback loop). Idempotent; covers HMR-added
8
- // styles too. Fire-and-forget.
6
+ // Rewrites every `vh` in <style> + CSSOM to a CSS variable-backed
7
+ // expression (kills the vh ↔ auto-resize feedback loop). Repeated calls
8
+ // update the variable, so callers can rebase without recovering raw CSS.
9
+ // Idempotent; covers HMR-added styles too. Fire-and-forget.
9
10
  //
10
11
  // parent → child { type: "measure-page-height", settleMs?: number }
11
- // After `settleMs` (default 2000) posts the page's scrollHeight back
12
+ // After `settleMs` (default 2000) posts the page's measured content height
12
13
  // as `{ type: "page-height-measured", height }` to the requester.
13
14
 
14
15
  type IncomingMessage = {
@@ -24,10 +25,17 @@ type IndexableCssRule = CSSRule & {
24
25
 
25
26
  type Debouncer = { trigger: () => void; cancel: () => void };
26
27
 
28
+ export type PageHeightBridgeController = {
29
+ freezeVhUnits: (override?: number) => void;
30
+ measurePageHeight: (origin: string, settleMs?: number) => void;
31
+ teardown: () => void;
32
+ };
33
+
27
34
  const FALLBACK_VHBASE: number = 900;
28
35
  const MIN_VHBASE: number = 400;
29
36
  const DEFAULT_SETTLE_MS: number = 2000;
30
37
  const NEUTRALIZE_DEBOUNCE_MS: number = 16;
38
+ const REFERENCE_VH_BASE_VAR: string = "--base44-reference-vh-base";
31
39
 
32
40
  const noop: () => void = (): void => {};
33
41
 
@@ -45,34 +53,7 @@ export function setupPageHeightBridge(): () => void {
45
53
  if (window.self === window.top) return noop;
46
54
  started = true;
47
55
 
48
- let vhCleanups: Array<() => void> | null = null;
49
- let vhForceRun: (() => void) | null = null;
50
- let pendingResponse: number | undefined;
51
- let pendingOrigin: string = "*";
52
-
53
- const freezeVhUnits = (override: number | undefined): void => {
54
- if (vhCleanups) {
55
- vhForceRun?.();
56
- return;
57
- }
58
- const referenceVhBase: number = resolveReferenceVhBase(override);
59
- vhCleanups = [];
60
- vhForceRun = startVhNeutralizer(referenceVhBase, vhCleanups);
61
- };
62
-
63
- // Target the requester's origin so the height isn't broadcast to anyone
64
- // who happens to embed us. Falls back to "*" when origin is unavailable
65
- // (jsdom default, sandboxed iframes with `null` origin).
66
- const measurePageHeight = (origin: string, settleMs: number): void => {
67
- pendingOrigin = origin;
68
- if (pendingResponse !== undefined) window.clearTimeout(pendingResponse);
69
- pendingResponse = window.setTimeout((): void => {
70
- requestAnimationFrame((): void => {
71
- const height: number = measureContentHeight();
72
- window.parent.postMessage({ type: "page-height-measured", height }, pendingOrigin);
73
- });
74
- }, settleMs);
75
- };
56
+ const controller: PageHeightBridgeController = createPageHeightBridgeController();
76
57
 
77
58
  const onMessage = (event: MessageEvent): void => {
78
59
  const data: IncomingMessage | null = (event.data ?? null) as IncomingMessage | null;
@@ -81,7 +62,7 @@ export function setupPageHeightBridge(): () => void {
81
62
  case "freeze-vh-units": {
82
63
  const override: number | undefined =
83
64
  typeof data.referenceVhBase === "number" ? data.referenceVhBase : undefined;
84
- freezeVhUnits(override);
65
+ controller.freezeVhUnits(override);
85
66
  return;
86
67
  }
87
68
  case "measure-page-height": {
@@ -89,7 +70,7 @@ export function setupPageHeightBridge(): () => void {
89
70
  typeof data.settleMs === "number" ? data.settleMs : DEFAULT_SETTLE_MS;
90
71
  const origin: string =
91
72
  event.origin && event.origin !== "null" ? event.origin : "*";
92
- measurePageHeight(origin, settleMs);
73
+ controller.measurePageHeight(origin, settleMs);
93
74
  return;
94
75
  }
95
76
  }
@@ -103,12 +84,50 @@ export function setupPageHeightBridge(): () => void {
103
84
  torn = true;
104
85
  started = false;
105
86
  window.removeEventListener("message", onMessage);
106
- if (vhCleanups) {
107
- for (const c of vhCleanups) c();
108
- vhCleanups = null;
109
- vhForceRun = null;
110
- }
111
- if (pendingResponse !== undefined) window.clearTimeout(pendingResponse);
87
+ controller.teardown();
88
+ };
89
+ }
90
+
91
+ export function createPageHeightBridgeController(): PageHeightBridgeController {
92
+ let vhCleanups: Array<() => void> | null = null;
93
+ let vhForceRun: (() => void) | null = null;
94
+ let pendingResponse: number | undefined;
95
+ let pendingOrigin: string = "*";
96
+
97
+ return {
98
+ freezeVhUnits: (override: number | undefined): void => {
99
+ const referenceVhBase: number = resolveReferenceVhBase(override);
100
+ setReferenceVhBase(referenceVhBase);
101
+ if (vhCleanups) {
102
+ vhForceRun?.();
103
+ return;
104
+ }
105
+ vhCleanups = [];
106
+ vhForceRun = startVhNeutralizer(vhCleanups);
107
+ },
108
+
109
+ // Target the requester's origin so the height isn't broadcast to anyone
110
+ // who happens to embed us. Falls back to "*" when origin is unavailable
111
+ // (jsdom default, sandboxed iframes with `null` origin).
112
+ measurePageHeight: (origin: string, settleMs: number = DEFAULT_SETTLE_MS): void => {
113
+ pendingOrigin = origin;
114
+ if (pendingResponse !== undefined) window.clearTimeout(pendingResponse);
115
+ pendingResponse = window.setTimeout((): void => {
116
+ requestAnimationFrame((): void => {
117
+ const height: number = measureContentHeight();
118
+ window.parent.postMessage({ type: "page-height-measured", height }, pendingOrigin);
119
+ });
120
+ }, settleMs);
121
+ },
122
+
123
+ teardown: (): void => {
124
+ if (vhCleanups) {
125
+ for (const c of vhCleanups) c();
126
+ vhCleanups = null;
127
+ vhForceRun = null;
128
+ }
129
+ if (pendingResponse !== undefined) window.clearTimeout(pendingResponse);
130
+ },
112
131
  };
113
132
  }
114
133
 
@@ -118,15 +137,16 @@ function resolveReferenceVhBase(override: number | undefined): number {
118
137
  return detected >= MIN_VHBASE ? detected : FALLBACK_VHBASE;
119
138
  }
120
139
 
140
+ function setReferenceVhBase(referenceVhBase: number): void {
141
+ document.documentElement.style.setProperty(REFERENCE_VH_BASE_VAR, `${referenceVhBase}px`);
142
+ }
143
+
121
144
  // Caches keep work proportional to *new* CSS, not total CSS. <head> observer
122
145
  // catches `<style>`/`<link>` adds; per-element observers catch HMR text edits.
123
146
  // Returns a `forceRun` so the caller can re-trigger neutralization on later
124
147
  // parent requests (idempotent — already-rewritten text is skipped via the
125
148
  // processed sets).
126
- function startVhNeutralizer(
127
- referenceVhBase: number,
128
- cleanups: Array<() => void>,
129
- ): () => void {
149
+ function startVhNeutralizer(cleanups: Array<() => void>): () => void {
130
150
  const VH_RE: RegExp = /(\d+(?:\.\d+)?)vh\b/g;
131
151
  const processedStyles: WeakSet<HTMLStyleElement> = new WeakSet();
132
152
  const processedSheets: WeakMap<CSSStyleSheet, number> = new WeakMap();
@@ -136,7 +156,7 @@ function startVhNeutralizer(
136
156
  const neutralize = (): void => {
137
157
  const rewrite = (input: string): string =>
138
158
  input.replace(VH_RE, (_match: string, n: string): string =>
139
- `${((parseFloat(n) / 100) * referenceVhBase).toFixed(2)}px`,
159
+ `calc(var(${REFERENCE_VH_BASE_VAR}) * ${formatVhFactor(n)})`,
140
160
  );
141
161
 
142
162
  document.querySelectorAll<HTMLStyleElement>("style").forEach((el: HTMLStyleElement): void => {
@@ -214,6 +234,10 @@ function startVhNeutralizer(
214
234
  return debouncedNeutralize;
215
235
  }
216
236
 
237
+ function formatVhFactor(value: string): string {
238
+ return String(Number((parseFloat(value) / 100).toFixed(6)));
239
+ }
240
+
217
241
  function rewriteVhInRules(rules: CSSRuleList, rewrite: (value: string) => string): void {
218
242
  for (let i: number = 0; i < rules.length; i++) {
219
243
  const rule: IndexableCssRule | undefined = rules[i] as IndexableCssRule | undefined;
@@ -241,10 +265,88 @@ function containsStylesheetNode(nodes: NodeList): boolean {
241
265
  }
242
266
 
243
267
  function measureContentHeight(): number {
244
- return Math.max(
268
+ const scrollHeight: number = Math.max(
245
269
  document.documentElement.scrollHeight,
246
270
  document.body?.scrollHeight ?? 0,
247
271
  );
272
+ const viewportHeight: number = Math.max(
273
+ window.innerHeight || 0,
274
+ document.documentElement.clientHeight,
275
+ document.body?.clientHeight ?? 0,
276
+ );
277
+ const contentBottom: number = measureElementContentBottom(viewportHeight);
278
+ const referenceVhBase: number = readReferenceVhBase();
279
+ if (contentBottom > 0) return Math.ceil(Math.max(contentBottom, referenceVhBase));
280
+ return Math.ceil(Math.max(scrollHeight, referenceVhBase));
281
+ }
282
+
283
+ function readReferenceVhBase(): number {
284
+ const value: string = document.documentElement.style.getPropertyValue(REFERENCE_VH_BASE_VAR);
285
+ const parsed: number = parseFloat(value);
286
+ return Number.isFinite(parsed) ? parsed : 0;
287
+ }
288
+
289
+ function measureElementContentBottom(viewportHeight: number): number {
290
+ if (!document.body) return 0;
291
+ const elements: Element[] = [document.body, ...Array.from(document.body.querySelectorAll("*"))];
292
+ const contentBottoms: WeakMap<Element, number> = new WeakMap();
293
+ const viewportBottom: number = window.scrollY + viewportHeight;
294
+
295
+ for (let i: number = elements.length - 1; i >= 0; i--) {
296
+ const el: Element | undefined = elements[i];
297
+ if (!el) continue;
298
+ const childContentBottom: number = readChildrenContentBottom(el, contentBottoms);
299
+ const rawBottom: number = readElementBottom(el);
300
+ const selfBottom: number = isViewportStretchedContainer(rawBottom, childContentBottom, viewportBottom)
301
+ ? 0
302
+ : rawBottom;
303
+ contentBottoms.set(el, Math.max(childContentBottom, selfBottom));
304
+ }
305
+
306
+ return contentBottoms.get(document.body) ?? 0;
307
+ }
308
+
309
+ function readChildrenContentBottom(
310
+ el: Element,
311
+ contentBottoms: WeakMap<Element, number>,
312
+ ): number {
313
+ let childContentBottom: number = 0;
314
+ for (let i: number = 0; i < el.children.length; i++) {
315
+ const child: Element | undefined = el.children[i];
316
+ if (!child) continue;
317
+ childContentBottom = Math.max(childContentBottom, contentBottoms.get(child) ?? 0);
318
+ }
319
+ return childContentBottom;
320
+ }
321
+
322
+ function readElementBottom(el: Element): number {
323
+ const computedStyle: CSSStyleDeclaration = window.getComputedStyle(el);
324
+ if (isOutOfFlowDecoration(computedStyle)) return 0;
325
+ const rect: DOMRect = el.getBoundingClientRect();
326
+ if (rect.width === 0 && rect.height === 0) return 0;
327
+ return rect.bottom + window.scrollY + readMarginBottom(computedStyle);
328
+ }
329
+
330
+ function readMarginBottom(computedStyle: CSSStyleDeclaration): number {
331
+ const marginBottom: number = parseFloat(computedStyle.marginBottom);
332
+ return Number.isFinite(marginBottom) ? marginBottom : 0;
333
+ }
334
+
335
+ function isOutOfFlowDecoration(computedStyle: CSSStyleDeclaration): boolean {
336
+ if (computedStyle.position === "fixed") return true;
337
+ return computedStyle.position === "absolute" && computedStyle.pointerEvents === "none";
338
+ }
339
+
340
+ function isViewportStretchedContainer(
341
+ elementBottom: number,
342
+ childBottom: number,
343
+ viewportBottom: number,
344
+ ): boolean {
345
+ return (
346
+ childBottom > 0 &&
347
+ Math.abs(elementBottom - viewportBottom) <= 1 &&
348
+ elementBottom - childBottom > 8
349
+ );
248
350
  }
249
351
 
250
352
  function createDebouncer(fn: () => void, delayMs: number): Debouncer {
@@ -3,13 +3,13 @@ import { createLayerController } from "./layer-dropdown/controller.js";
3
3
  import { LAYER_DROPDOWN_ATTR } from "./layer-dropdown/consts.js";
4
4
  import { createInlineEditController } from "../capabilities/inline-edit/index.js";
5
5
  import { THEME_FONT_PREVIEW_ID } from "../consts.js";
6
- import { setupPageHeightBridge } from "./page-height-bridge.js";
6
+ import { createPageHeightBridgeController } from "./page-height-bridge.js";
7
7
 
8
8
  const REPOSITION_DELAY_MS = 50;
9
9
 
10
10
  export function setupVisualEditAgent() {
11
- setupPageHeightBridge();
12
-
11
+ const pageHeightBridge = createPageHeightBridgeController();
12
+
13
13
  // State variables (replacing React useState/useRef)
14
14
  let isVisualEditMode = false;
15
15
  let isPopoverDragging = false;
@@ -641,6 +641,21 @@ export function setupVisualEditAgent() {
641
641
  }
642
642
  break;
643
643
 
644
+ case "freeze-vh-units":
645
+ pageHeightBridge.freezeVhUnits(
646
+ typeof message.referenceVhBase === "number"
647
+ ? message.referenceVhBase
648
+ : undefined
649
+ );
650
+ break;
651
+
652
+ case "measure-page-height":
653
+ pageHeightBridge.measurePageHeight(
654
+ event.origin && event.origin !== "null" ? event.origin : "*",
655
+ typeof message.settleMs === "number" ? message.settleMs : undefined
656
+ );
657
+ break;
658
+
644
659
  default:
645
660
  break;
646
661
  }
@@ -726,4 +741,4 @@ export function setupVisualEditAgent() {
726
741
 
727
742
  // Send ready message to parent
728
743
  window.parent.postMessage({ type: "visual-edit-agent-ready" }, "*");
729
- }
744
+ }