@proto-labs-inc/rig-core 0.1.1 → 0.2.1
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/frame-tools-loader.d.ts +2 -0
- package/dist/frame-tools-loader.js +47 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +10 -353
- package/package.json +2 -2
- package/dist/element-preview.d.ts +0 -20
- package/dist/element-preview.js +0 -95
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Stable bootstrap only. Selection behavior is deployed with the Frame app. */
|
|
2
|
+
export function loadFrameTools(frameOrigin) {
|
|
3
|
+
if (window.parent === window)
|
|
4
|
+
return () => { };
|
|
5
|
+
let disposed = false;
|
|
6
|
+
let unmount;
|
|
7
|
+
const report = (status, origin = "*") => {
|
|
8
|
+
if (!disposed)
|
|
9
|
+
window.parent.postMessage({ type: "frame-tools-status", status }, origin);
|
|
10
|
+
};
|
|
11
|
+
const dispose = () => { disposed = true; unmount?.(); };
|
|
12
|
+
if (!frameOrigin) {
|
|
13
|
+
report("unavailable");
|
|
14
|
+
return dispose;
|
|
15
|
+
}
|
|
16
|
+
let origin;
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(frameOrigin);
|
|
19
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
20
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
21
|
+
throw new Error("Invalid Frame origin");
|
|
22
|
+
if (url.username || url.password || url.origin !== frameOrigin)
|
|
23
|
+
throw new Error("Expected an origin");
|
|
24
|
+
origin = url.origin;
|
|
25
|
+
// The origin is pinned in the prototype, never taken from a message or URL parameter.
|
|
26
|
+
if (!document.referrer || new URL(document.referrer).origin !== origin)
|
|
27
|
+
return dispose;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
report("unavailable");
|
|
31
|
+
return dispose;
|
|
32
|
+
}
|
|
33
|
+
report("loading", origin);
|
|
34
|
+
const url = new URL("/frame-tools/v1.js", origin).href;
|
|
35
|
+
void import(/* @vite-ignore */ url).then((runtime) => {
|
|
36
|
+
if (disposed)
|
|
37
|
+
return;
|
|
38
|
+
unmount = runtime.mountFrameTools({ frameOrigin: origin });
|
|
39
|
+
report("ready", origin);
|
|
40
|
+
}).catch((error) => {
|
|
41
|
+
if (disposed)
|
|
42
|
+
return;
|
|
43
|
+
console.warn("Proto selection tools failed to load", error);
|
|
44
|
+
report("unavailable", origin);
|
|
45
|
+
});
|
|
46
|
+
return dispose;
|
|
47
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The rig's framework-free core: URL-state sync, the bridge, and
|
|
3
|
-
*
|
|
2
|
+
* The rig's framework-free core: URL-state sync, the bridge, and the Frame tools
|
|
3
|
+
* loader. Everything here is plain DOM + history + postMessage — no framework.
|
|
4
4
|
* Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
|
|
5
5
|
* this with their own idioms and nothing else.
|
|
6
6
|
*/
|
|
7
7
|
import { type Manifest, type ResolvedPreviewTheme, type RigToFrame } from "@proto-labs-inc/wire";
|
|
8
8
|
export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
|
|
9
9
|
export declare const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
|
|
10
|
+
/** Mark the page light or dark (data-proto-theme and the "dark" class) for
|
|
11
|
+
* a prototype that styles both. The page's own CSS decides color-scheme:
|
|
12
|
+
* forcing it here gave pages that only style light mode dark form fields
|
|
13
|
+
* on a light page whenever the viewer's system was dark. */
|
|
10
14
|
export declare function applyPreviewTheme(theme: ResolvedPreviewTheme): void;
|
|
11
15
|
/** All addressable state, mirrored to the URL. Keys: "state" | "v.<component>". */
|
|
12
16
|
export declare class RigStore {
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The rig's framework-free core: URL-state sync, the bridge, and
|
|
3
|
-
*
|
|
2
|
+
* The rig's framework-free core: URL-state sync, the bridge, and the Frame tools
|
|
3
|
+
* loader. Everything here is plain DOM + history + postMessage — no framework.
|
|
4
4
|
* Framework adapters (@proto-labs-inc/rig for React, @proto-labs-inc/rig-vue for Vue) wrap
|
|
5
5
|
* this with their own idioms and nothing else.
|
|
6
6
|
*/
|
|
7
7
|
import { PROTOCOL_VERSION, STATE_PARAM, variantParam, applyVariantSelection, isFrameToRig, } from "@proto-labs-inc/wire";
|
|
8
|
-
import {
|
|
8
|
+
import { loadFrameTools } from "./frame-tools-loader.js";
|
|
9
9
|
export { STATE_PARAM, variantParam } from "@proto-labs-inc/wire";
|
|
10
|
-
const RIG_VERSION = "0.
|
|
10
|
+
const RIG_VERSION = "0.2.1";
|
|
11
11
|
export const INTERNAL_PREVIEW_THEME_PARAM = "__protoTheme";
|
|
12
|
+
/** Mark the page light or dark (data-proto-theme and the "dark" class) for
|
|
13
|
+
* a prototype that styles both. The page's own CSS decides color-scheme:
|
|
14
|
+
* forcing it here gave pages that only style light mode dark form fields
|
|
15
|
+
* on a light page whenever the viewer's system was dark. */
|
|
12
16
|
export function applyPreviewTheme(theme) {
|
|
13
17
|
if (typeof document === "undefined")
|
|
14
18
|
return;
|
|
15
19
|
const root = document.documentElement;
|
|
16
20
|
root.dataset.protoTheme = theme;
|
|
17
21
|
root.classList.toggle("dark", theme === "dark");
|
|
18
|
-
root.style.colorScheme = theme;
|
|
19
22
|
}
|
|
20
23
|
function resolvedSystemTheme() {
|
|
21
24
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
@@ -131,48 +134,6 @@ export function postToFrame(msg) {
|
|
|
131
134
|
if (typeof window !== "undefined" && window.parent !== window)
|
|
132
135
|
window.parent.postMessage(msg, "*");
|
|
133
136
|
}
|
|
134
|
-
// ─── comment mode ──────────────────────────────────────────────────
|
|
135
|
-
// The Frame can't touch this document (cross-origin iframe), so the blue
|
|
136
|
-
// selector runs here: hit-test [data-proto-id] under the cursor, draw the
|
|
137
|
-
// highlight, and on click capture the view with the highlight baked in.
|
|
138
|
-
// The highlight is Proto blue (#0088FF): 2px solid at 55%, a 6% fill,
|
|
139
|
-
// radius 4px, 0.12s ease-out entry; the picker layer sits at z-index 99997.
|
|
140
|
-
const COMMENT_BLUE = "#0088FF";
|
|
141
|
-
const TARGET_ATTR = "data-proto-id";
|
|
142
|
-
function selectorFor(element) {
|
|
143
|
-
if (element === document.documentElement)
|
|
144
|
-
return "html";
|
|
145
|
-
if (element === document.body)
|
|
146
|
-
return "body";
|
|
147
|
-
const protoId = element.getAttribute(TARGET_ATTR);
|
|
148
|
-
if (protoId)
|
|
149
|
-
return `[${TARGET_ATTR}="${CSS.escape(protoId)}"]`;
|
|
150
|
-
if (element.id) {
|
|
151
|
-
const idSelector = `#${CSS.escape(element.id)}`;
|
|
152
|
-
if (document.querySelectorAll(idSelector).length === 1)
|
|
153
|
-
return idSelector;
|
|
154
|
-
}
|
|
155
|
-
const owner = element.closest(`[${TARGET_ATTR}]`);
|
|
156
|
-
const boundary = owner ?? document.body;
|
|
157
|
-
const segments = [];
|
|
158
|
-
let current = element;
|
|
159
|
-
while (current && current !== boundary) {
|
|
160
|
-
const tag = current.tagName.toLowerCase();
|
|
161
|
-
const currentTagName = current.tagName;
|
|
162
|
-
const siblings = current.parentElement
|
|
163
|
-
? [...current.parentElement.children].filter((sibling) => sibling.tagName === currentTagName)
|
|
164
|
-
: [];
|
|
165
|
-
const index = siblings.indexOf(current);
|
|
166
|
-
segments.unshift(siblings.length > 1 && index >= 0 ? `${tag}:nth-of-type(${index + 1})` : tag);
|
|
167
|
-
current = current.parentElement;
|
|
168
|
-
}
|
|
169
|
-
const base = owner
|
|
170
|
-
? `[${TARGET_ATTR}="${CSS.escape(owner.getAttribute(TARGET_ATTR) ?? "")}"]`
|
|
171
|
-
: boundary === element
|
|
172
|
-
? element.tagName.toLowerCase()
|
|
173
|
-
: "body";
|
|
174
|
-
return segments.length > 0 ? `${base} > ${segments.join(" > ")}` : base;
|
|
175
|
-
}
|
|
176
137
|
async function waitForCaptureReady() {
|
|
177
138
|
const pendingImages = [...document.images]
|
|
178
139
|
.filter((image) => !image.complete)
|
|
@@ -190,47 +151,6 @@ async function waitForCaptureReady() {
|
|
|
190
151
|
// resting state before the gallery captures them.
|
|
191
152
|
await new Promise((resolve) => window.setTimeout(resolve, 1500));
|
|
192
153
|
}
|
|
193
|
-
async function captureViewport(scale = Math.min(window.devicePixelRatio || 1, 2)) {
|
|
194
|
-
try {
|
|
195
|
-
// Optional peer provided by the prototype, not the rig.
|
|
196
|
-
const mod = (await import("modern-screenshot"));
|
|
197
|
-
const root = document.body;
|
|
198
|
-
const viewportWidth = document.documentElement.clientWidth;
|
|
199
|
-
const viewportHeight = document.documentElement.clientHeight;
|
|
200
|
-
const scrollX = window.scrollX || document.documentElement.scrollLeft;
|
|
201
|
-
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
|
202
|
-
const contentWidth = Math.max(viewportWidth, document.documentElement.scrollWidth, root.scrollWidth);
|
|
203
|
-
const contentHeight = Math.max(viewportHeight, document.documentElement.scrollHeight, root.scrollHeight);
|
|
204
|
-
return await mod.domToDataUrl(root, {
|
|
205
|
-
scale,
|
|
206
|
-
width: viewportWidth,
|
|
207
|
-
height: viewportHeight,
|
|
208
|
-
features: { restoreScrollPosition: true },
|
|
209
|
-
style: {
|
|
210
|
-
margin: "0",
|
|
211
|
-
width: `${contentWidth}px`,
|
|
212
|
-
height: `${contentHeight}px`,
|
|
213
|
-
transform: `translate(${-scrollX}px, ${-scrollY}px)`,
|
|
214
|
-
transformOrigin: "top left",
|
|
215
|
-
overflow: "visible",
|
|
216
|
-
},
|
|
217
|
-
onCloneNode: (cloned) => {
|
|
218
|
-
if (!(cloned instanceof HTMLElement) || (!scrollX && !scrollY))
|
|
219
|
-
return;
|
|
220
|
-
const fixed = [cloned, ...cloned.querySelectorAll("*")].filter((element) => element.style.position === "fixed");
|
|
221
|
-
for (const element of fixed) {
|
|
222
|
-
const existing = element.style.transform;
|
|
223
|
-
element.style.transform =
|
|
224
|
-
`translate(${scrollX}px, ${scrollY}px)` +
|
|
225
|
-
(existing && existing !== "none" ? ` ${existing}` : "");
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
154
|
async function captureThumbnail() {
|
|
235
155
|
const restingState = document.createElement("style");
|
|
236
156
|
restingState.textContent = `
|
|
@@ -277,229 +197,13 @@ async function captureThumbnail() {
|
|
|
277
197
|
restingState.remove();
|
|
278
198
|
}
|
|
279
199
|
}
|
|
280
|
-
const elementPreview = new ElementPreview(postToFrame);
|
|
281
|
-
class ElementPicker {
|
|
282
|
-
overlay = null;
|
|
283
|
-
highlight = null;
|
|
284
|
-
cursorStyle = null;
|
|
285
|
-
target = null;
|
|
286
|
-
active = false;
|
|
287
|
-
mode = null;
|
|
288
|
-
enable(mode) {
|
|
289
|
-
if (typeof document === "undefined")
|
|
290
|
-
return;
|
|
291
|
-
elementPreview.release();
|
|
292
|
-
if (this.active || this.overlay)
|
|
293
|
-
this.disable();
|
|
294
|
-
this.active = true;
|
|
295
|
-
this.mode = mode;
|
|
296
|
-
this.cursorStyle = document.createElement("style");
|
|
297
|
-
this.cursorStyle.setAttribute("data-proto-picker-cursor", "");
|
|
298
|
-
this.cursorStyle.textContent =
|
|
299
|
-
"html, body, body * { cursor: crosshair !important; }";
|
|
300
|
-
document.head.appendChild(this.cursorStyle);
|
|
301
|
-
const blue = COMMENT_BLUE;
|
|
302
|
-
this.overlay = document.createElement("div");
|
|
303
|
-
this.overlay.setAttribute("data-proto-rig-overlay", "");
|
|
304
|
-
Object.assign(this.overlay.style, {
|
|
305
|
-
position: "fixed",
|
|
306
|
-
inset: "0",
|
|
307
|
-
zIndex: "99997",
|
|
308
|
-
pointerEvents: "none",
|
|
309
|
-
});
|
|
310
|
-
this.highlight = document.createElement("div");
|
|
311
|
-
Object.assign(this.highlight.style, {
|
|
312
|
-
position: "absolute",
|
|
313
|
-
border: `2px solid color-mix(in srgb, ${blue} 55%, transparent)`,
|
|
314
|
-
borderRadius: "4px",
|
|
315
|
-
backgroundColor: `color-mix(in srgb, ${blue} 6%, transparent)`,
|
|
316
|
-
pointerEvents: "none",
|
|
317
|
-
boxSizing: "border-box",
|
|
318
|
-
opacity: "0",
|
|
319
|
-
transition: "opacity 0.12s ease-out",
|
|
320
|
-
willChange: "opacity",
|
|
321
|
-
});
|
|
322
|
-
this.overlay.appendChild(this.highlight);
|
|
323
|
-
document.body.appendChild(this.overlay);
|
|
324
|
-
document.addEventListener("pointermove", this.onMove, true);
|
|
325
|
-
document.addEventListener("click", this.onClick, true);
|
|
326
|
-
document.addEventListener("keydown", this.onKey, true);
|
|
327
|
-
}
|
|
328
|
-
disable() {
|
|
329
|
-
if (!this.active && !this.overlay && !this.cursorStyle)
|
|
330
|
-
return;
|
|
331
|
-
this.active = false;
|
|
332
|
-
document.removeEventListener("pointermove", this.onMove, true);
|
|
333
|
-
document.removeEventListener("click", this.onClick, true);
|
|
334
|
-
document.removeEventListener("keydown", this.onKey, true);
|
|
335
|
-
this.overlay?.remove();
|
|
336
|
-
this.cursorStyle?.remove();
|
|
337
|
-
this.overlay = null;
|
|
338
|
-
this.highlight = null;
|
|
339
|
-
this.cursorStyle = null;
|
|
340
|
-
this.target = null;
|
|
341
|
-
this.mode = null;
|
|
342
|
-
}
|
|
343
|
-
freezeSelection() {
|
|
344
|
-
this.active = false;
|
|
345
|
-
document.removeEventListener("pointermove", this.onMove, true);
|
|
346
|
-
document.removeEventListener("click", this.onClick, true);
|
|
347
|
-
document.removeEventListener("keydown", this.onKey, true);
|
|
348
|
-
this.cursorStyle?.remove();
|
|
349
|
-
this.cursorStyle = null;
|
|
350
|
-
}
|
|
351
|
-
onMove = (e) => {
|
|
352
|
-
const hit = this.targetAt(e.clientX, e.clientY);
|
|
353
|
-
if (hit === this.target)
|
|
354
|
-
return;
|
|
355
|
-
this.target = hit;
|
|
356
|
-
if (!this.highlight)
|
|
357
|
-
return;
|
|
358
|
-
const rect = this.rectFor(hit);
|
|
359
|
-
Object.assign(this.highlight.style, {
|
|
360
|
-
left: `${rect.left}px`,
|
|
361
|
-
top: `${rect.top}px`,
|
|
362
|
-
width: `${rect.width}px`,
|
|
363
|
-
height: `${rect.height}px`,
|
|
364
|
-
opacity: "1",
|
|
365
|
-
});
|
|
366
|
-
};
|
|
367
|
-
onClick = (e) => {
|
|
368
|
-
e.preventDefault();
|
|
369
|
-
e.stopPropagation();
|
|
370
|
-
const clickedTarget = this.targetAt(e.clientX, e.clientY);
|
|
371
|
-
this.target = clickedTarget;
|
|
372
|
-
const targetId = clickedTarget?.getAttribute(TARGET_ATTR) ?? null;
|
|
373
|
-
if (this.mode === "element") {
|
|
374
|
-
const rect = this.rectFor(clickedTarget);
|
|
375
|
-
const text = clickedTarget.textContent?.replace(/\s+/g, " ").trim() || null;
|
|
376
|
-
const inspection = elementPreview.inspect(clickedTarget);
|
|
377
|
-
this.disable();
|
|
378
|
-
postToFrame({
|
|
379
|
-
type: "element-target-picked",
|
|
380
|
-
inspection,
|
|
381
|
-
selector: selectorFor(clickedTarget),
|
|
382
|
-
tagName: clickedTarget.tagName.toLowerCase(),
|
|
383
|
-
text: text?.slice(0, 160) ?? null,
|
|
384
|
-
anchor: {
|
|
385
|
-
x: rect.left,
|
|
386
|
-
y: rect.top,
|
|
387
|
-
width: rect.width,
|
|
388
|
-
height: rect.height,
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (this.mode === "variantSet") {
|
|
394
|
-
if (!targetId)
|
|
395
|
-
return;
|
|
396
|
-
const rect = this.rectFor(clickedTarget);
|
|
397
|
-
this.disable();
|
|
398
|
-
postToFrame({
|
|
399
|
-
type: "variant-set-target-picked",
|
|
400
|
-
targetId,
|
|
401
|
-
anchor: {
|
|
402
|
-
x: rect.left,
|
|
403
|
-
y: rect.top,
|
|
404
|
-
width: rect.width,
|
|
405
|
-
height: rect.height,
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
const rect = this.rectFor(clickedTarget);
|
|
411
|
-
if (this.highlight) {
|
|
412
|
-
Object.assign(this.highlight.style, {
|
|
413
|
-
left: `${rect.left}px`,
|
|
414
|
-
top: `${rect.top}px`,
|
|
415
|
-
width: `${rect.width}px`,
|
|
416
|
-
height: `${rect.height}px`,
|
|
417
|
-
opacity: "1",
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
this.freezeSelection();
|
|
421
|
-
postToFrame({
|
|
422
|
-
type: "comment-target-picked",
|
|
423
|
-
targetId,
|
|
424
|
-
anchor: {
|
|
425
|
-
x: rect.left,
|
|
426
|
-
y: rect.top,
|
|
427
|
-
width: rect.width,
|
|
428
|
-
height: rect.height,
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
};
|
|
432
|
-
targetAt(x, y) {
|
|
433
|
-
const hit = document.elementFromPoint(x, y);
|
|
434
|
-
if (this.mode === "element")
|
|
435
|
-
return hit ?? document.body;
|
|
436
|
-
return hit?.closest(`[${TARGET_ATTR}]`) ?? document.body;
|
|
437
|
-
}
|
|
438
|
-
rectFor(target) {
|
|
439
|
-
if (target === document.body || target === document.documentElement) {
|
|
440
|
-
return {
|
|
441
|
-
left: 0,
|
|
442
|
-
top: 0,
|
|
443
|
-
width: document.documentElement.clientWidth,
|
|
444
|
-
height: document.documentElement.clientHeight,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
return target.getBoundingClientRect();
|
|
448
|
-
}
|
|
449
|
-
onKey = (e) => {
|
|
450
|
-
if (e.key !== "Escape")
|
|
451
|
-
return;
|
|
452
|
-
e.preventDefault();
|
|
453
|
-
const mode = this.mode;
|
|
454
|
-
this.disable();
|
|
455
|
-
postToFrame({
|
|
456
|
-
type: mode === "variantSet"
|
|
457
|
-
? "variant-set-picker-exited"
|
|
458
|
-
: mode === "element"
|
|
459
|
-
? "element-picker-exited"
|
|
460
|
-
: "comment-mode-exited",
|
|
461
|
-
});
|
|
462
|
-
};
|
|
463
|
-
async captureSelection(requestId) {
|
|
464
|
-
if (!this.target)
|
|
465
|
-
return;
|
|
466
|
-
const targetId = this.target.getAttribute(TARGET_ATTR);
|
|
467
|
-
const rect = this.rectFor(this.target);
|
|
468
|
-
const anchor = {
|
|
469
|
-
x: rect.left,
|
|
470
|
-
y: rect.top,
|
|
471
|
-
width: rect.width,
|
|
472
|
-
height: rect.height,
|
|
473
|
-
};
|
|
474
|
-
if (this.highlight) {
|
|
475
|
-
Object.assign(this.highlight.style, {
|
|
476
|
-
left: `${rect.left}px`,
|
|
477
|
-
top: `${rect.top}px`,
|
|
478
|
-
width: `${rect.width}px`,
|
|
479
|
-
height: `${rect.height}px`,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
// The page is already interactive when the user picks a comment target.
|
|
483
|
-
// Capture immediately: thumbnail readiness delays can consume the Frame's
|
|
484
|
-
// request timeout on image-heavy prototypes.
|
|
485
|
-
const image = await captureViewport();
|
|
486
|
-
this.disable();
|
|
487
|
-
postToFrame({
|
|
488
|
-
type: "comment-capture",
|
|
489
|
-
targetId,
|
|
490
|
-
image,
|
|
491
|
-
requestId,
|
|
492
|
-
anchor,
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
const elementPicker = typeof window === "undefined" ? null : new ElementPicker();
|
|
497
200
|
/**
|
|
498
201
|
* Wire a store to the bridge: listen for Frame messages, announce the rig,
|
|
499
202
|
* and report the boot-time state (the deep-link fix). Returns a disposer.
|
|
500
203
|
* Call client-side only.
|
|
501
204
|
*/
|
|
502
205
|
export function wireRig(store, manifest) {
|
|
206
|
+
const disposeTools = loadFrameTools(manifest.frameOrigin);
|
|
503
207
|
let controlledTheme = false;
|
|
504
208
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
505
209
|
const onSystemTheme = () => {
|
|
@@ -512,16 +216,6 @@ export function wireRig(store, manifest) {
|
|
|
512
216
|
return;
|
|
513
217
|
if (!isFrameToRig(e.data))
|
|
514
218
|
return; // tolerate unknown messages silently
|
|
515
|
-
if (e.data.type === "preview-element-styles") {
|
|
516
|
-
elementPreview.preview(e.data.selectionId, e.data.styles);
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
if (e.data.type === "release-element") {
|
|
520
|
-
elementPreview.release(e.data.selectionId);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
if (["set-state", "set-variant", "set-theme"].includes(e.data.type))
|
|
524
|
-
elementPreview.release();
|
|
525
219
|
if (e.data.type === "set-state") {
|
|
526
220
|
const state = manifest.states.find((s) => s.id === e.data.id);
|
|
527
221
|
store.setState(e.data.id, state?.variants ?? {});
|
|
@@ -532,42 +226,6 @@ export function wireRig(store, manifest) {
|
|
|
532
226
|
}
|
|
533
227
|
else if (e.data.type === "set-variant")
|
|
534
228
|
store.set(variantParam(e.data.component), e.data.variant);
|
|
535
|
-
else if (e.data.type === "measure-target") {
|
|
536
|
-
const target = Array.from(document.querySelectorAll(`[${TARGET_ATTR}]`)).find((candidate) => candidate.getAttribute(TARGET_ATTR) === e.data.id);
|
|
537
|
-
const rect = target?.getBoundingClientRect();
|
|
538
|
-
postToFrame({
|
|
539
|
-
type: "target-measured",
|
|
540
|
-
targetId: e.data.id,
|
|
541
|
-
anchor: rect
|
|
542
|
-
? {
|
|
543
|
-
x: rect.left,
|
|
544
|
-
y: rect.top,
|
|
545
|
-
width: rect.width,
|
|
546
|
-
height: rect.height,
|
|
547
|
-
}
|
|
548
|
-
: null,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
else if (e.data.type === "set-comment-mode") {
|
|
552
|
-
if (e.data.on)
|
|
553
|
-
elementPicker?.enable("comment");
|
|
554
|
-
else
|
|
555
|
-
elementPicker?.disable();
|
|
556
|
-
}
|
|
557
|
-
else if (e.data.type === "capture-comment")
|
|
558
|
-
void elementPicker?.captureSelection(e.data.requestId);
|
|
559
|
-
else if (e.data.type === "set-variant-set-picker") {
|
|
560
|
-
if (e.data.on)
|
|
561
|
-
elementPicker?.enable("variantSet");
|
|
562
|
-
else
|
|
563
|
-
elementPicker?.disable();
|
|
564
|
-
}
|
|
565
|
-
else if (e.data.type === "set-element-picker") {
|
|
566
|
-
if (e.data.on)
|
|
567
|
-
elementPicker?.enable("element");
|
|
568
|
-
else
|
|
569
|
-
elementPicker?.disable();
|
|
570
|
-
}
|
|
571
229
|
};
|
|
572
230
|
window.addEventListener("message", onMessage);
|
|
573
231
|
postToFrame({
|
|
@@ -590,7 +248,6 @@ export function wireRig(store, manifest) {
|
|
|
590
248
|
return () => {
|
|
591
249
|
media.removeEventListener("change", onSystemTheme);
|
|
592
250
|
window.removeEventListener("message", onMessage);
|
|
593
|
-
|
|
594
|
-
elementPreview.release();
|
|
251
|
+
disposeTools();
|
|
595
252
|
};
|
|
596
253
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proto-labs-inc/rig-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Proto's rig, framework-free: URL-state sync, the postMessage bridge to the Frame, and comment mode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@proto-labs-inc/wire": "^0.
|
|
31
|
+
"@proto-labs-inc/wire": "^0.2.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "~6.0.2",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type ElementStyles, type RigToFrame } from "@proto-labs-inc/wire";
|
|
2
|
-
/** Owns one temporary edit. Never resolves a selector back to a different element. */
|
|
3
|
-
export declare class ElementPreview {
|
|
4
|
-
private post;
|
|
5
|
-
private session;
|
|
6
|
-
private observer;
|
|
7
|
-
private resize;
|
|
8
|
-
private animationFrame;
|
|
9
|
-
private lastBounds;
|
|
10
|
-
constructor(post: (message: RigToFrame) => void);
|
|
11
|
-
inspect(target: Element): {
|
|
12
|
-
selectionId: `${string}-${string}-${string}-${string}-${string}`;
|
|
13
|
-
styles: Partial<Record<"color" | "background-color" | "opacity" | "font-family" | "font-size" | "font-weight" | "line-height" | "letter-spacing" | "padding-top" | "padding-right" | "padding-bottom" | "padding-left" | "border-radius" | "gap", string>>;
|
|
14
|
-
} | undefined;
|
|
15
|
-
preview(id: string, styles: ElementStyles): void;
|
|
16
|
-
private restore;
|
|
17
|
-
release(id?: string): void;
|
|
18
|
-
private scheduleBounds;
|
|
19
|
-
private measure;
|
|
20
|
-
}
|
package/dist/element-preview.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { ELEMENT_STYLE_PROPERTIES } from "@proto-labs-inc/wire";
|
|
2
|
-
/** Owns one temporary edit. Never resolves a selector back to a different element. */
|
|
3
|
-
export class ElementPreview {
|
|
4
|
-
post;
|
|
5
|
-
session = null;
|
|
6
|
-
observer = null;
|
|
7
|
-
resize = null;
|
|
8
|
-
animationFrame = 0;
|
|
9
|
-
lastBounds = "";
|
|
10
|
-
constructor(post) {
|
|
11
|
-
this.post = post;
|
|
12
|
-
}
|
|
13
|
-
inspect(target) {
|
|
14
|
-
this.release();
|
|
15
|
-
if (!(target instanceof HTMLElement || target instanceof SVGElement))
|
|
16
|
-
return undefined;
|
|
17
|
-
const computed = getComputedStyle(target);
|
|
18
|
-
const hasText = Array.from(target.childNodes).some((node) => node.nodeType === Node.TEXT_NODE && Boolean(node.textContent?.trim())) || target.matches("input, textarea, select");
|
|
19
|
-
const typography = new Set(["color", "font-family", "font-size", "font-weight", "line-height", "letter-spacing"]);
|
|
20
|
-
const styles = {};
|
|
21
|
-
for (const property of ELEMENT_STYLE_PROPERTIES) {
|
|
22
|
-
if (typography.has(property) && !hasText)
|
|
23
|
-
continue;
|
|
24
|
-
if (property === "gap" && !/flex|grid/.test(computed.display))
|
|
25
|
-
continue;
|
|
26
|
-
styles[property] = computed.getPropertyValue(property);
|
|
27
|
-
}
|
|
28
|
-
const selectionId = crypto.randomUUID();
|
|
29
|
-
this.session = { id: selectionId, target, original: target.getAttribute("style") };
|
|
30
|
-
this.lastBounds = "";
|
|
31
|
-
this.observer = new MutationObserver(this.scheduleBounds);
|
|
32
|
-
this.observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
33
|
-
this.resize = new ResizeObserver(this.scheduleBounds);
|
|
34
|
-
this.resize.observe(target);
|
|
35
|
-
window.addEventListener("scroll", this.scheduleBounds, true);
|
|
36
|
-
window.addEventListener("resize", this.scheduleBounds);
|
|
37
|
-
return { selectionId, styles };
|
|
38
|
-
}
|
|
39
|
-
preview(id, styles) {
|
|
40
|
-
const session = this.session;
|
|
41
|
-
if (!session || session.id !== id)
|
|
42
|
-
return;
|
|
43
|
-
this.restore();
|
|
44
|
-
for (const property of ELEMENT_STYLE_PROPERTIES) {
|
|
45
|
-
const value = styles[property];
|
|
46
|
-
if (value && CSS.supports(property, value))
|
|
47
|
-
session.target.style.setProperty(property, value, "important");
|
|
48
|
-
}
|
|
49
|
-
this.scheduleBounds();
|
|
50
|
-
}
|
|
51
|
-
restore() {
|
|
52
|
-
if (!this.session)
|
|
53
|
-
return;
|
|
54
|
-
const { target, original } = this.session;
|
|
55
|
-
if (original === null)
|
|
56
|
-
target.removeAttribute("style");
|
|
57
|
-
else
|
|
58
|
-
target.setAttribute("style", original);
|
|
59
|
-
}
|
|
60
|
-
release(id) {
|
|
61
|
-
if (!this.session || (id && id !== this.session.id))
|
|
62
|
-
return;
|
|
63
|
-
const selectionId = this.session.id;
|
|
64
|
-
this.observer?.disconnect();
|
|
65
|
-
this.resize?.disconnect();
|
|
66
|
-
window.removeEventListener("scroll", this.scheduleBounds, true);
|
|
67
|
-
window.removeEventListener("resize", this.scheduleBounds);
|
|
68
|
-
cancelAnimationFrame(this.animationFrame);
|
|
69
|
-
this.animationFrame = 0;
|
|
70
|
-
this.restore();
|
|
71
|
-
this.session = null;
|
|
72
|
-
this.post({ type: "element-preview-bounds", selectionId, anchor: null });
|
|
73
|
-
}
|
|
74
|
-
scheduleBounds = () => {
|
|
75
|
-
if (!this.animationFrame)
|
|
76
|
-
this.animationFrame = requestAnimationFrame(this.measure);
|
|
77
|
-
};
|
|
78
|
-
measure = () => {
|
|
79
|
-
this.animationFrame = 0;
|
|
80
|
-
if (!this.session)
|
|
81
|
-
return;
|
|
82
|
-
const { target, id } = this.session;
|
|
83
|
-
if (!target.isConnected) {
|
|
84
|
-
this.release();
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const rect = target.getBoundingClientRect();
|
|
88
|
-
const anchor = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
89
|
-
const bounds = JSON.stringify(anchor);
|
|
90
|
-
if (bounds === this.lastBounds)
|
|
91
|
-
return;
|
|
92
|
-
this.lastBounds = bounds;
|
|
93
|
-
this.post({ type: "element-preview-bounds", selectionId: id, anchor });
|
|
94
|
-
};
|
|
95
|
-
}
|