@valbuild/next 0.101.1 → 0.103.0

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 (35) hide show
  1. package/README.md +50 -1
  2. package/client/dist/valbuild-next-client.cjs.dev.js +25 -2
  3. package/client/dist/valbuild-next-client.cjs.prod.js +25 -2
  4. package/client/dist/valbuild-next-client.esm.js +25 -2
  5. package/dist/ValApp-90c4171d.cjs.js +7 -0
  6. package/dist/{ValApp-bb207e96.cjs.prod.js → ValApp-90c4171d.cjs.prod.js} +8 -6
  7. package/dist/{ValApp-1992d8b3.cjs.dev.js → ValApp-c1c3ef55.cjs.dev.js} +8 -6
  8. package/dist/{ValApp-8912fc8b.esm.js → ValApp-e17eb734.esm.js} +5 -3
  9. package/dist/ValCanvasBridge-96f67a41.cjs.dev.js +395 -0
  10. package/dist/ValCanvasBridge-d8713d9d.esm.js +387 -0
  11. package/dist/ValCanvasBridge-f75509c6.cjs.js +7 -0
  12. package/dist/ValCanvasBridge-f75509c6.cjs.prod.js +395 -0
  13. package/dist/{ValNextProvider-7302b8af.esm.js → ValNextProvider-3209948c.esm.js} +282 -43
  14. package/dist/{ValNextProvider-03a9f8fd.cjs.dev.js → ValNextProvider-3d8ebfec.cjs.dev.js} +297 -58
  15. package/dist/ValNextProvider-3fdf8756.cjs.js +7 -0
  16. package/dist/{ValNextProvider-6103cb74.cjs.prod.js → ValNextProvider-3fdf8756.cjs.prod.js} +297 -58
  17. package/dist/ValOverlayContext-1d019b95.cjs.js +7 -0
  18. package/dist/{ValOverlayContext-f7f45bc7.cjs.prod.js → ValOverlayContext-1d019b95.cjs.prod.js} +8 -2
  19. package/dist/{ValOverlayContext-c6f27a6d.cjs.dev.js → ValOverlayContext-ca3b6ebd.cjs.dev.js} +8 -2
  20. package/dist/{ValOverlayContext-3c37e5a7.esm.js → ValOverlayContext-f63e0fc9.esm.js} +8 -2
  21. package/dist/declarations/src/ValApp.d.ts +1 -1
  22. package/dist/declarations/src/ValImage.d.ts +1 -1
  23. package/dist/declarations/src/ValProvider.d.ts +1 -1
  24. package/dist/declarations/src/external_exempt_from_val_quickjs.d.ts +7 -5
  25. package/dist/fallbackColors-5ead2c26.esm.js +58 -0
  26. package/dist/{cssUtils-9c11102f.cjs.dev.js → fallbackColors-9a2ac53a.cjs.dev.js} +30 -0
  27. package/dist/{cssUtils-d97a757c.cjs.prod.js → fallbackColors-d4a5b0da.cjs.prod.js} +30 -0
  28. package/dist/valbuild-next.cjs.dev.js +8 -13
  29. package/dist/valbuild-next.cjs.prod.js +8 -13
  30. package/dist/valbuild-next.esm.js +9 -10
  31. package/package.json +13 -13
  32. package/dist/ValApp-bb207e96.cjs.js +0 -7
  33. package/dist/ValNextProvider-6103cb74.cjs.js +0 -7
  34. package/dist/ValOverlayContext-f7f45bc7.cjs.js +0 -7
  35. package/dist/cssUtils-b5651c03.esm.js +0 -32
@@ -0,0 +1,387 @@
1
+ 'use client';
2
+ import { _ as _slicedToArray } from './slicedToArray-aa291011.esm.js';
3
+ import React from 'react';
4
+ import { Internal } from '@valbuild/core';
5
+ import { VAL_CANVAS_MESSAGE, isValCanvasStudioMessage } from '@valbuild/shared/internal';
6
+ import { jsx } from 'react/jsx-runtime';
7
+ import './unsupportedIterableToArray-5baabfdc.esm.js';
8
+
9
+ var SELECTION = "#079455";
10
+ var SELECTION_SOFT = "rgba(7, 148, 85, 0.4)";
11
+
12
+ /**
13
+ * How often the page is re-measured even when nothing said it changed.
14
+ *
15
+ * A backstop, not the mechanism — the observers below carry the responsive
16
+ * case. Slow enough to be free, fast enough that a field which appeared late is
17
+ * in the list before anyone goes looking for it.
18
+ */
19
+ var RESCAN_INTERVAL_MS = 5000;
20
+
21
+ /**
22
+ * The page's half of the canvas protocol.
23
+ *
24
+ * Mounted only in the studio's canvas frame. Its job is to tell the studio
25
+ * where Val's content is on the page, and to do the two things the studio can
26
+ * ask of a document it cannot touch: outline an element, and treat a click as a
27
+ * pick rather than as a click.
28
+ *
29
+ * Nothing here is visible on the customer's own site — this component is not
30
+ * mounted there at all.
31
+ */
32
+ function ValCanvasBridge(_ref) {
33
+ var draftMode = _ref.draftMode,
34
+ isRefreshing = _ref.isRefreshing;
35
+ var _React$useState = React.useState(false),
36
+ _React$useState2 = _slicedToArray(_React$useState, 2),
37
+ picking = _React$useState2[0],
38
+ setPicking = _React$useState2[1];
39
+ var _React$useState3 = React.useState(null),
40
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
41
+ highlighted = _React$useState4[0],
42
+ setHighlighted = _React$useState4[1];
43
+ // Kept in a ref as well, because the capture-phase click listener below is
44
+ // installed once and would otherwise close over the first value forever.
45
+ var pickingRef = React.useRef(false);
46
+ pickingRef.current = picking;
47
+ var post = React.useCallback(function (message) {
48
+ // `*` as the target origin: the studio and the page share an origin today,
49
+ // but the frame does not know the parent's origin for certain and naming
50
+ // the wrong one silently drops the message. Nothing sent here is
51
+ // sensitive — it is the positions of elements on a page the parent is
52
+ // already displaying.
53
+ window.parent.postMessage(message, "*");
54
+ }, []);
55
+
56
+ /**
57
+ * The last payload posted, as a string.
58
+ *
59
+ * The periodic scan below runs whether or not anything moved, and an
60
+ * unchanged payload costs the studio a re-render of the fields column for
61
+ * nothing. Compared as a string because the comparison has to be by value —
62
+ * every scan builds fresh objects.
63
+ */
64
+ var lastPosted = React.useRef(null);
65
+
66
+ /** Every element Val tagged, with where it is on the page. */
67
+ var scan = React.useCallback(function () {
68
+ var elements = [];
69
+ var scrollX = window.scrollX;
70
+ var scrollY = window.scrollY;
71
+ document.querySelectorAll("[data-val-path]").forEach(function (el) {
72
+ if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) return;
73
+ var attribute = el.getAttribute("data-val-path");
74
+ if (!attribute) return;
75
+ /**
76
+ * A `<source>` has no box of its own, so its `<video>`/`<picture>` parent
77
+ * is measured instead — the same substitution the overlay makes, and for
78
+ * the same reason: an element with a zero-size rect cannot be pointed at.
79
+ */
80
+ var measured = el;
81
+ if (el.tagName === "SOURCE") {
82
+ var parent = el.parentElement;
83
+ if (parent && (parent.tagName === "VIDEO" || parent.tagName === "PICTURE")) {
84
+ measured = parent;
85
+ }
86
+ }
87
+ var rect = measured.getBoundingClientRect();
88
+ if (rect.width === 0 && rect.height === 0) return;
89
+ elements.push({
90
+ // Split by the same helper the rest of Val uses, so the studio gets
91
+ // the paths back in exactly the form it holds them in.
92
+ paths: Internal.splitJoinedSourcePaths(attribute),
93
+ // Page coordinates, not viewport: the studio scales and offsets the
94
+ // whole document, so a viewport-relative box would be wrong the moment
95
+ // the frame scrolled.
96
+ rect: {
97
+ top: rect.top + scrollY,
98
+ left: rect.left + scrollX,
99
+ width: rect.width,
100
+ height: rect.height
101
+ }
102
+ });
103
+ });
104
+ var message = {
105
+ val: VAL_CANVAS_MESSAGE,
106
+ type: "elements",
107
+ elements: elements,
108
+ pageSize: {
109
+ width: document.documentElement.scrollWidth,
110
+ height: document.documentElement.scrollHeight
111
+ }
112
+ };
113
+ var serialized = JSON.stringify(message);
114
+ if (serialized === lastPosted.current) {
115
+ return;
116
+ }
117
+ lastPosted.current = serialized;
118
+ post(message);
119
+ }, [post]);
120
+
121
+ // Relayed as it changes, not polled: the studio only has to know the two
122
+ // edges, and a message per render would be a message per keystroke.
123
+ React.useEffect(function () {
124
+ post({
125
+ val: VAL_CANVAS_MESSAGE,
126
+ type: "refreshing",
127
+ pending: isRefreshing === true
128
+ });
129
+ }, [post, isRefreshing]);
130
+
131
+ // Announce the frame, and say whether it is actually showing draft content.
132
+ React.useEffect(function () {
133
+ post({
134
+ val: VAL_CANVAS_MESSAGE,
135
+ type: "ready",
136
+ draftMode: draftMode,
137
+ url: window.location.href
138
+ });
139
+ }, [post, draftMode]);
140
+
141
+ /**
142
+ * Re-scan whenever the page could have moved.
143
+ *
144
+ * Coalesced into one animation frame: a layout change fires mutation,
145
+ * resize and scroll events together, and scanning three times produces three
146
+ * identical messages.
147
+ */
148
+ React.useEffect(function () {
149
+ var frame = null;
150
+ var schedule = function schedule() {
151
+ if (frame !== null) return;
152
+ frame = requestAnimationFrame(function () {
153
+ frame = null;
154
+ scan();
155
+ });
156
+ };
157
+ schedule();
158
+ var mutations = new MutationObserver(schedule);
159
+ mutations.observe(document.body, {
160
+ subtree: true,
161
+ childList: true,
162
+ attributes: true,
163
+ attributeFilter: ["data-val-path", "class", "style"]
164
+ });
165
+ var resizes = new ResizeObserver(schedule);
166
+ resizes.observe(document.documentElement);
167
+ window.addEventListener("scroll", schedule, {
168
+ passive: true
169
+ });
170
+ window.addEventListener("resize", schedule);
171
+ // Images and fonts land after the first paint and move everything below
172
+ // them, so the boxes measured before `load` are stale by definition.
173
+ window.addEventListener("load", schedule);
174
+ return function () {
175
+ if (frame !== null) cancelAnimationFrame(frame);
176
+ mutations.disconnect();
177
+ resizes.disconnect();
178
+ window.removeEventListener("scroll", schedule);
179
+ window.removeEventListener("resize", schedule);
180
+ window.removeEventListener("load", schedule);
181
+ };
182
+ }, [scan]);
183
+
184
+ /**
185
+ * And re-scan every few seconds regardless.
186
+ *
187
+ * The event-driven scan above is not enough in practice, and the way it fails
188
+ * is confusing rather than obviously broken: the fields column shows some of
189
+ * what is on the page and not the rest, until you happen to edit one of the
190
+ * missing ones and they appear. Anything that tags content after the observers
191
+ * are set up but produces no mutation they watch — a streamed-in server
192
+ * component, an element that measured zero because its image had not decoded,
193
+ * content revealed by something outside `data-val-path`, `class` and `style` —
194
+ * is invisible until the next unrelated change. A slow poll costs nothing and
195
+ * closes all of those at once, without having to guess which one it was.
196
+ *
197
+ * Idle-scheduled, and never on the frame's critical path. A scan is a
198
+ * `querySelectorAll` plus a forced layout per tagged element, which is exactly
199
+ * the kind of work that should wait for a gap rather than take one — and this
200
+ * runs inside the customer's own page, where Val is a guest. Skipped entirely
201
+ * while the frame is hidden, where the boxes cannot have moved and nobody is
202
+ * looking at them.
203
+ */
204
+ React.useEffect(function () {
205
+ var idle = null;
206
+ var runWhenIdle = function runWhenIdle() {
207
+ if (document.hidden || idle !== null) return;
208
+ // `requestIdleCallback` where it exists, a timeout where it does not
209
+ // (Safari). The timeout option matters: without it a busy page can defer
210
+ // an idle callback indefinitely, which is the one outcome worse than
211
+ // scanning at a bad moment.
212
+ if (typeof window.requestIdleCallback === "function") {
213
+ idle = window.requestIdleCallback(function () {
214
+ idle = null;
215
+ scan();
216
+ }, {
217
+ timeout: 1000
218
+ });
219
+ } else {
220
+ idle = window.setTimeout(function () {
221
+ idle = null;
222
+ scan();
223
+ }, 0);
224
+ }
225
+ };
226
+ var interval = window.setInterval(runWhenIdle, RESCAN_INTERVAL_MS);
227
+ return function () {
228
+ window.clearInterval(interval);
229
+ if (idle === null) return;
230
+ if (typeof window.cancelIdleCallback === "function") {
231
+ window.cancelIdleCallback(idle);
232
+ } else {
233
+ window.clearTimeout(idle);
234
+ }
235
+ };
236
+ }, [scan]);
237
+
238
+ // What the studio asks for.
239
+ React.useEffect(function () {
240
+ var listener = function listener(event) {
241
+ if (!isValCanvasStudioMessage(event.data)) return;
242
+ var message = event.data;
243
+ if (message.type === "rescan") {
244
+ scan();
245
+ return;
246
+ }
247
+ if (message.type === "setPicking") {
248
+ setPicking(message.picking);
249
+ return;
250
+ }
251
+ if (message.type === "sourceUpdate") {
252
+ /**
253
+ * Handed straight to the listener the page already has.
254
+ *
255
+ * `ValNextProvider` listens for this event on its own window to make an
256
+ * inline edit visible behind the overlay; the only thing different here
257
+ * is that the edit was made in another window. Re-dispatching rather
258
+ * than reaching into the store keeps one path into the page — including
259
+ * the `router.refresh()` it throttles behind it, which is what brings a
260
+ * server component's own re-read across.
261
+ */
262
+ window.dispatchEvent(new CustomEvent("val-event", {
263
+ detail: {
264
+ type: "source-update",
265
+ moduleFilePath: message.moduleFilePath,
266
+ source: message.source
267
+ }
268
+ }));
269
+ return;
270
+ }
271
+ if (message.type === "sourcesSynced") {
272
+ // Relayed the same way and for the same reason as `sourceUpdate`: the
273
+ // page is listening on its own window, and the studio is in another.
274
+ window.dispatchEvent(new CustomEvent("val-event", {
275
+ detail: {
276
+ type: "sources-synced"
277
+ }
278
+ }));
279
+ return;
280
+ }
281
+ setHighlighted(message.path);
282
+ if (message.path !== null && message.scrollIntoView) {
283
+ var target = findByPath(message.path);
284
+ target === null || target === void 0 || target.scrollIntoView({
285
+ block: "center",
286
+ behavior: "smooth"
287
+ });
288
+ }
289
+ };
290
+ window.addEventListener("message", listener);
291
+ return function () {
292
+ return window.removeEventListener("message", listener);
293
+ };
294
+ }, [scan]);
295
+
296
+ /**
297
+ * Picking.
298
+ *
299
+ * Capture phase, and the event is stopped: the point of picking mode is that
300
+ * a click means "this one" rather than what the page would do with it, and a
301
+ * link that navigates has taken the canvas somewhere else before the studio
302
+ * hears anything.
303
+ */
304
+ React.useEffect(function () {
305
+ var onClick = function onClick(event) {
306
+ if (!pickingRef.current) return;
307
+ var target = event.target;
308
+ if (!(target instanceof Element)) return;
309
+ var tagged = target.closest("[data-val-path]");
310
+ var attribute = tagged === null || tagged === void 0 ? void 0 : tagged.getAttribute("data-val-path");
311
+ if (!attribute) return;
312
+ event.preventDefault();
313
+ event.stopPropagation();
314
+ post({
315
+ val: VAL_CANVAS_MESSAGE,
316
+ type: "clicked",
317
+ paths: Internal.splitJoinedSourcePaths(attribute)
318
+ });
319
+ };
320
+ document.addEventListener("click", onClick, {
321
+ capture: true
322
+ });
323
+ return function () {
324
+ return document.removeEventListener("click", onClick, {
325
+ capture: true
326
+ });
327
+ };
328
+ }, [post]);
329
+
330
+ /**
331
+ * The outlines, as a stylesheet rather than as injected elements.
332
+ *
333
+ * A style rule cannot disturb the page's layout, which an absolutely
334
+ * positioned box on top of it eventually would — and the whole value of
335
+ * showing the real page is that it is laid out the way the customer built
336
+ * it. `outline` rather than `border` for the same reason: outlines do not
337
+ * take up space.
338
+ */
339
+ return /*#__PURE__*/jsx("style", {
340
+ // The attribute selector is built from the path, so it is escaped as a
341
+ // CSS string. A path contains quotes (`?p="/"`) and would otherwise
342
+ // close the selector early.
343
+ dangerouslySetInnerHTML: {
344
+ __html: [picking ? "[data-val-path] { outline: 1px solid ".concat(SELECTION_SOFT, "; outline-offset: 1px; cursor: pointer; }\n[data-val-path]:hover { outline: 2px solid ").concat(SELECTION, "; }") : "", highlighted ? highlightRule(highlighted) : ""].filter(Boolean).join("\n")
345
+ }
346
+ });
347
+ }
348
+
349
+ /**
350
+ * A rule that outlines exactly the elements carrying `path`.
351
+ *
352
+ * The attribute holds a comma-separated list, so this cannot be one `=`
353
+ * comparison; and it cannot be a `*=` substring test either, because a path is
354
+ * a prefix of every path below it. `?p="/blogs/blog1"` is contained in
355
+ * `?p="/blogs/blog1"."title"`, so a substring test on the route would outline
356
+ * every field on the page — which is what it did.
357
+ *
358
+ * Four selectors instead: the whole attribute, the first item, the last item,
359
+ * and one in the middle. Between them they match the path as a complete list
360
+ * item and nothing else.
361
+ */
362
+ function highlightRule(path) {
363
+ var value = JSON.stringify(path);
364
+ var inner = JSON.stringify(",".concat(path, ","));
365
+ var first = JSON.stringify("".concat(path, ","));
366
+ var last = JSON.stringify(",".concat(path));
367
+ var selectors = ["[data-val-path=".concat(value, "]"), "[data-val-path^=".concat(first, "]"), "[data-val-path$=".concat(last, "]"), "[data-val-path*=".concat(inner, "]")].join(",\n");
368
+ return "".concat(selectors, " { outline: 2px solid ").concat(SELECTION, "; outline-offset: 1px; }");
369
+ }
370
+ function findByPath(path) {
371
+ var elements = document.querySelectorAll("[data-val-path]");
372
+ for (var _i = 0, _Array$from = Array.from(elements); _i < _Array$from.length; _i++) {
373
+ var el = _Array$from[_i];
374
+ var attribute = el.getAttribute("data-val-path");
375
+ if (!attribute) continue;
376
+ // Compared as strings: the split returns branded `SourcePath`s and what
377
+ // came back over `postMessage` is a plain string, which is the same value.
378
+ if (Internal.splitJoinedSourcePaths(attribute).some(function (candidate) {
379
+ return candidate === path;
380
+ })) {
381
+ return el;
382
+ }
383
+ }
384
+ return null;
385
+ }
386
+
387
+ export { ValCanvasBridge };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ if (process.env.NODE_ENV === "production") {
4
+ module.exports = require("./ValCanvasBridge-f75509c6.cjs.prod.js");
5
+ } else {
6
+ module.exports = require("./ValCanvasBridge-f75509c6.cjs.dev.js");
7
+ }