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